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
19pub struct PagesOptions {
21 pub file: String,
23 pub page: Option<u64>,
25 pub verbose: bool,
27 pub show_empty: bool,
29 pub list_mode: bool,
31 pub filter_type: Option<String>,
33 pub page_size: Option<u32>,
35 pub json: bool,
37 pub keyring: Option<String>,
39}
40
41#[derive(Serialize)]
43struct PageDetailJson {
44 page_number: u64,
45 header: FilHeader,
46 page_type_name: String,
47 page_type_description: String,
48 byte_start: u64,
49 byte_end: u64,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 index_header: Option<IndexHeader>,
52 #[serde(skip_serializing_if = "Option::is_none")]
53 fsp_header: Option<FspHeader>,
54}
55
56pub fn execute(opts: &PagesOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
80 let mut ts = match opts.page_size {
81 Some(ps) => Tablespace::open_with_page_size(&opts.file, ps)?,
82 None => Tablespace::open(&opts.file)?,
83 };
84
85 if let Some(ref keyring_path) = opts.keyring {
86 crate::cli::setup_decryption(&mut ts, keyring_path)?;
87 }
88
89 let page_size = ts.page_size();
90
91 if opts.json {
92 return execute_json(opts, &mut ts, page_size, writer);
93 }
94
95 if let Some(page_num) = opts.page {
96 let page_data = ts.read_page(page_num)?;
97 print_full_page(&page_data, page_num, page_size, opts.verbose, writer)?;
98 return Ok(());
99 }
100
101 if opts.filter_type.is_none() {
103 let page0 = ts.read_page(0)?;
104 if let Some(fsp) = FspHeader::parse(&page0) {
105 print_fsp_header_detail(&fsp, &page0, opts.verbose, ts.vendor_info(), writer)?;
106 }
107 }
108
109 for page_num in 0..ts.page_count() {
110 let page_data = ts.read_page(page_num)?;
111 let header = match FilHeader::parse(&page_data) {
112 Some(h) => h,
113 None => continue,
114 };
115
116 if !opts.show_empty && header.checksum == 0 && header.page_type == PageType::Allocated {
118 continue;
119 }
120
121 if let Some(ref filter) = opts.filter_type {
123 if !matches_page_type_filter(&header.page_type, filter) {
124 continue;
125 }
126 }
127
128 if opts.list_mode {
129 print_list_line(&page_data, page_num, page_size, writer)?;
130 } else {
131 print_full_page(&page_data, page_num, page_size, opts.verbose, writer)?;
132 }
133 }
134
135 Ok(())
136}
137
138fn execute_json(
140 opts: &PagesOptions,
141 ts: &mut Tablespace,
142 page_size: u32,
143 writer: &mut dyn Write,
144) -> Result<(), IdbError> {
145 let mut pages = Vec::new();
146
147 let range: Box<dyn Iterator<Item = u64>> = if let Some(p) = opts.page {
148 Box::new(std::iter::once(p))
149 } else {
150 Box::new(0..ts.page_count())
151 };
152
153 for page_num in range {
154 let page_data = ts.read_page(page_num)?;
155 let header = match FilHeader::parse(&page_data) {
156 Some(h) => h,
157 None => continue,
158 };
159
160 if !opts.show_empty && header.checksum == 0 && header.page_type == PageType::Allocated {
161 continue;
162 }
163
164 if let Some(ref filter) = opts.filter_type {
165 if !matches_page_type_filter(&header.page_type, filter) {
166 continue;
167 }
168 }
169
170 let pt = header.page_type;
171 let byte_start = page_num * page_size as u64;
172
173 let index_header = if pt == PageType::Index {
174 IndexHeader::parse(&page_data)
175 } else {
176 None
177 };
178
179 let fsp_header = if page_num == 0 {
180 FspHeader::parse(&page_data)
181 } else {
182 None
183 };
184
185 pages.push(PageDetailJson {
186 page_number: page_num,
187 page_type_name: pt.name().to_string(),
188 page_type_description: pt.description().to_string(),
189 byte_start,
190 byte_end: byte_start + page_size as u64,
191 header,
192 index_header,
193 fsp_header,
194 });
195 }
196
197 let json = serde_json::to_string_pretty(&pages)
198 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
199 wprintln!(writer, "{}", json)?;
200 Ok(())
201}
202
203fn print_list_line(
205 page_data: &[u8],
206 page_num: u64,
207 page_size: u32,
208 writer: &mut dyn Write,
209) -> Result<(), IdbError> {
210 let header = match FilHeader::parse(page_data) {
211 Some(h) => h,
212 None => return Ok(()),
213 };
214
215 let pt = header.page_type;
216 let byte_start = page_num * page_size as u64;
217
218 wprint!(
219 writer,
220 "-- Page {} - {}: {}",
221 page_num,
222 pt.name(),
223 pt.description()
224 )?;
225
226 if pt == PageType::Index {
227 if let Some(idx) = IndexHeader::parse(page_data) {
228 wprint!(writer, ", Index ID: {}", idx.index_id)?;
229 }
230 }
231
232 wprintln!(writer, ", Byte Start: {}", format_offset(byte_start))?;
233 Ok(())
234}
235
236fn print_full_page(
238 page_data: &[u8],
239 page_num: u64,
240 page_size: u32,
241 verbose: bool,
242 writer: &mut dyn Write,
243) -> Result<(), IdbError> {
244 let header = match FilHeader::parse(page_data) {
245 Some(h) => h,
246 None => {
247 eprintln!("Could not parse FIL header for page {}", page_num);
248 return Ok(());
249 }
250 };
251
252 let byte_start = page_num * page_size as u64;
253 let byte_end = byte_start + page_size as u64;
254 let pt = header.page_type;
255
256 wprintln!(writer)?;
258 wprintln!(writer, "=== HEADER: Page {}", header.page_number)?;
259 wprintln!(writer, "Byte Start: {}", format_offset(byte_start))?;
260 wprintln!(
261 writer,
262 "Page Type: {}\n-- {}: {} - {}",
263 pt.as_u16(),
264 pt.name(),
265 pt.description(),
266 pt.usage()
267 )?;
268
269 wprint!(writer, "Prev Page: ")?;
270 if !header.has_prev() {
271 wprintln!(writer, "Not used.")?;
272 } else {
273 wprintln!(writer, "{}", header.prev_page)?;
274 }
275
276 wprint!(writer, "Next Page: ")?;
277 if !header.has_next() {
278 wprintln!(writer, "Not used.")?;
279 } else {
280 wprintln!(writer, "{}", header.next_page)?;
281 }
282
283 wprintln!(writer, "LSN: {}", header.lsn)?;
284 wprintln!(writer, "Space ID: {}", header.space_id)?;
285 wprintln!(writer, "Checksum: {}", header.checksum)?;
286
287 if pt == PageType::Index {
289 if let Some(idx) = IndexHeader::parse(page_data) {
290 wprintln!(writer)?;
291 print_index_header(&idx, header.page_number, verbose, writer)?;
292
293 wprintln!(writer)?;
294 print_fseg_headers(page_data, header.page_number, &idx, verbose, writer)?;
295
296 wprintln!(writer)?;
297 print_system_records(page_data, header.page_number, writer)?;
298 }
299 }
300
301 if matches!(pt, PageType::Blob | PageType::ZBlob | PageType::ZBlob2) {
303 if let Some(blob_hdr) = BlobPageHeader::parse(page_data) {
304 wprintln!(writer)?;
305 wprintln!(writer, "=== BLOB Header: Page {}", header.page_number)?;
306 wprintln!(writer, "Data Length: {} bytes", blob_hdr.part_len)?;
307 if blob_hdr.has_next() {
308 wprintln!(writer, "Next BLOB Page: {}", blob_hdr.next_page_no)?;
309 } else {
310 wprintln!(writer, "Next BLOB Page: None (last in chain)")?;
311 }
312 }
313 }
314
315 if pt == PageType::LobFirst {
317 if let Some(lob_hdr) = LobFirstPageHeader::parse(page_data) {
318 wprintln!(writer)?;
319 wprintln!(
320 writer,
321 "=== LOB First Page Header: Page {}",
322 header.page_number
323 )?;
324 wprintln!(writer, "Version: {}", lob_hdr.version)?;
325 wprintln!(writer, "Flags: {}", lob_hdr.flags)?;
326 wprintln!(writer, "Total Data Length: {} bytes", lob_hdr.data_len)?;
327 if lob_hdr.trx_id > 0 {
328 wprintln!(writer, "Transaction ID: {}", lob_hdr.trx_id)?;
329 }
330 }
331 }
332
333 if pt == PageType::UndoLog {
335 if let Some(undo_hdr) = UndoPageHeader::parse(page_data) {
336 wprintln!(writer)?;
337 wprintln!(writer, "=== UNDO Header: Page {}", header.page_number)?;
338 wprintln!(
339 writer,
340 "Undo Type: {} ({})",
341 undo_hdr.page_type.name(),
342 undo_hdr.page_type.name()
343 )?;
344 wprintln!(writer, "Log Start Offset: {}", undo_hdr.start)?;
345 wprintln!(writer, "Free Offset: {}", undo_hdr.free)?;
346 wprintln!(
347 writer,
348 "Used Bytes: {}",
349 undo_hdr.free.saturating_sub(undo_hdr.start)
350 )?;
351
352 if let Some(seg_hdr) = UndoSegmentHeader::parse(page_data) {
353 wprintln!(writer, "Segment State: {}", seg_hdr.state.name())?;
354 wprintln!(writer, "Last Log Offset: {}", seg_hdr.last_log)?;
355 }
356 }
357 }
358
359 wprintln!(writer)?;
361 let ps = page_size as usize;
362 if page_data.len() >= ps {
363 let trailer_offset = ps - 8;
364 if let Some(trailer) = crate::innodb::page::FilTrailer::parse(&page_data[trailer_offset..])
365 {
366 wprintln!(writer, "=== TRAILER: Page {}", header.page_number)?;
367 wprintln!(writer, "Old-style Checksum: {}", trailer.checksum)?;
368 wprintln!(writer, "Low 32 bits of LSN: {}", trailer.lsn_low32)?;
369 wprintln!(writer, "Byte End: {}", format_offset(byte_end))?;
370
371 if verbose {
372 let csum_result = checksum::validate_checksum(page_data, page_size, None);
373 let status = if csum_result.valid {
374 "OK".green().to_string()
375 } else {
376 "MISMATCH".red().to_string()
377 };
378 wprintln!(
379 writer,
380 "Checksum Status: {} ({:?})",
381 status,
382 csum_result.algorithm
383 )?;
384
385 let lsn_valid = checksum::validate_lsn(page_data, page_size);
386 let lsn_status = if lsn_valid {
387 "OK".green().to_string()
388 } else {
389 "MISMATCH".red().to_string()
390 };
391 wprintln!(writer, "LSN Consistency: {}", lsn_status)?;
392 }
393 }
394 }
395
396 Ok(())
397}
398
399fn print_index_header(
401 idx: &IndexHeader,
402 page_num: u32,
403 verbose: bool,
404 writer: &mut dyn Write,
405) -> Result<(), IdbError> {
406 wprintln!(writer, "=== INDEX Header: Page {}", page_num)?;
407 wprintln!(writer, "Index ID: {}", idx.index_id)?;
408 wprintln!(writer, "Node Level: {}", idx.level)?;
409
410 if idx.max_trx_id > 0 {
411 wprintln!(writer, "Max Transaction ID: {}", idx.max_trx_id)?;
412 } else {
413 wprintln!(writer, "-- Secondary Index")?;
414 }
415
416 wprintln!(writer, "Directory Slots: {}", idx.n_dir_slots)?;
417 if verbose {
418 wprintln!(writer, "-- Number of slots in page directory")?;
419 }
420
421 wprintln!(writer, "Heap Top: {}", idx.heap_top)?;
422 if verbose {
423 wprintln!(writer, "-- Pointer to record heap top")?;
424 }
425
426 wprintln!(writer, "Records in Page: {}", idx.n_recs)?;
427 wprintln!(
428 writer,
429 "Records in Heap: {} (compact: {})",
430 idx.n_heap(),
431 idx.is_compact()
432 )?;
433 if verbose {
434 wprintln!(writer, "-- Number of records in heap")?;
435 }
436
437 wprintln!(writer, "Start of Free Record List: {}", idx.free)?;
438 wprintln!(writer, "Garbage Bytes: {}", idx.garbage)?;
439 if verbose {
440 wprintln!(writer, "-- Number of bytes in deleted records.")?;
441 }
442
443 wprintln!(writer, "Last Insert: {}", idx.last_insert)?;
444 wprintln!(
445 writer,
446 "Last Insert Direction: {} - {}",
447 idx.direction,
448 idx.direction_name()
449 )?;
450 wprintln!(writer, "Inserts in this direction: {}", idx.n_direction)?;
451 if verbose {
452 wprintln!(
453 writer,
454 "-- Number of consecutive inserts in this direction."
455 )?;
456 }
457
458 Ok(())
459}
460
461fn print_fseg_headers(
463 page_data: &[u8],
464 page_num: u32,
465 idx: &IndexHeader,
466 verbose: bool,
467 writer: &mut dyn Write,
468) -> Result<(), IdbError> {
469 wprintln!(
470 writer,
471 "=== FSEG_HDR - File Segment Header: Page {}",
472 page_num
473 )?;
474
475 if let Some(leaf) = FsegHeader::parse_leaf(page_data) {
476 wprintln!(writer, "Inode Space ID: {}", leaf.space_id)?;
477 wprintln!(writer, "Inode Page Number: {}", leaf.page_no)?;
478 wprintln!(writer, "Inode Offset: {}", leaf.offset)?;
479 }
480
481 if idx.is_leaf() {
482 if let Some(internal) = FsegHeader::parse_internal(page_data) {
483 wprintln!(writer, "Non-leaf Space ID: {}", internal.space_id)?;
484 if verbose {
485 wprintln!(writer, "Non-leaf Page Number: {}", internal.page_no)?;
486 wprintln!(writer, "Non-leaf Offset: {}", internal.offset)?;
487 }
488 }
489 }
490
491 Ok(())
492}
493
494fn print_system_records(
496 page_data: &[u8],
497 page_num: u32,
498 writer: &mut dyn Write,
499) -> Result<(), IdbError> {
500 let sys = match SystemRecords::parse(page_data) {
501 Some(s) => s,
502 None => return Ok(()),
503 };
504
505 wprintln!(writer, "=== INDEX System Records: Page {}", page_num)?;
506 wprintln!(
507 writer,
508 "Index Record Status: {} - (Decimal: {}) {}",
509 sys.rec_status,
510 sys.rec_status,
511 sys.rec_status_name()
512 )?;
513 wprintln!(writer, "Number of records owned: {}", sys.n_owned)?;
514 wprintln!(writer, "Deleted: {}", if sys.deleted { "1" } else { "0" })?;
515 wprintln!(writer, "Heap Number: {}", sys.heap_no)?;
516 wprintln!(writer, "Next Record Offset (Infimum): {}", sys.infimum_next)?;
517 wprintln!(
518 writer,
519 "Next Record Offset (Supremum): {}",
520 sys.supremum_next
521 )?;
522 wprintln!(
523 writer,
524 "Left-most node on non-leaf level: {}",
525 if sys.min_rec { "1" } else { "0" }
526 )?;
527
528 Ok(())
529}
530
531fn print_fsp_header_detail(
533 fsp: &FspHeader,
534 page0: &[u8],
535 verbose: bool,
536 vendor_info: &crate::innodb::vendor::VendorInfo,
537 writer: &mut dyn Write,
538) -> Result<(), IdbError> {
539 wprintln!(writer, "=== File Header")?;
540 wprintln!(writer, "Vendor: {}", vendor_info)?;
541 wprintln!(writer, "Space ID: {}", fsp.space_id)?;
542 if verbose {
543 wprintln!(writer, "-- Offset 38, Length 4")?;
544 }
545 wprintln!(writer, "Size: {}", fsp.size)?;
546 wprintln!(writer, "Flags: {}", fsp.flags)?;
547 wprintln!(
548 writer,
549 "Page Free Limit: {} (this should always be 64 on a single-table file)",
550 fsp.free_limit
551 )?;
552
553 let comp = compression::detect_compression(fsp.flags, Some(vendor_info));
555 let enc = encryption::detect_encryption(fsp.flags, Some(vendor_info));
556 if comp != compression::CompressionAlgorithm::None {
557 wprintln!(writer, "Compression: {}", comp)?;
558 }
559 if enc != encryption::EncryptionAlgorithm::None {
560 wprintln!(writer, "Encryption: {}", enc)?;
561
562 if let Some(info) = encryption::parse_encryption_info(page0, fsp.page_size_from_flags_with_vendor(vendor_info)) {
564 let version_desc = match info.magic_version {
565 1 => "V1",
566 2 => "V2",
567 3 => "V3 (MySQL 8.0.5+)",
568 _ => "Unknown",
569 };
570 wprintln!(writer, " Master Key ID: {}", info.master_key_id)?;
571 wprintln!(writer, " Server UUID: {}", info.server_uuid)?;
572 wprintln!(writer, " Magic: {}", version_desc)?;
573 }
574 }
575
576 let seg_id_offset = crate::innodb::constants::FIL_PAGE_DATA + 72;
578 if page0.len() >= seg_id_offset + 8 {
579 use byteorder::ByteOrder;
580 let seg_id = byteorder::BigEndian::read_u64(&page0[seg_id_offset..]);
581 wprintln!(writer, "First Unused Segment ID: {}", seg_id)?;
582 }
583
584 Ok(())
585}
586
587fn matches_page_type_filter(page_type: &PageType, filter: &str) -> bool {
592 let filter_upper = filter.to_uppercase();
593 let type_name = page_type.name();
594
595 if type_name == filter_upper {
597 return true;
598 }
599
600 match filter_upper.as_str() {
602 "UNDO" => *page_type == PageType::UndoLog,
603 "BLOB" => matches!(
604 page_type,
605 PageType::Blob | PageType::ZBlob | PageType::ZBlob2
606 ),
607 "LOB" => matches!(
608 page_type,
609 PageType::LobIndex | PageType::LobData | PageType::LobFirst
610 ),
611 "SDI" => matches!(page_type, PageType::Sdi | PageType::SdiBlob),
612 "COMPRESSED" | "COMP" => matches!(
613 page_type,
614 PageType::Compressed
615 | PageType::CompressedEncrypted
616 | PageType::PageCompressed
617 | PageType::PageCompressedEncrypted
618 ),
619 "ENCRYPTED" | "ENC" => matches!(
620 page_type,
621 PageType::Encrypted
622 | PageType::CompressedEncrypted
623 | PageType::EncryptedRtree
624 | PageType::PageCompressedEncrypted
625 ),
626 "INSTANT" => *page_type == PageType::Instant,
627 _ => type_name.contains(&filter_upper),
628 }
629}