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}
38
39#[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
54pub 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 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 if !opts.show_empty && header.checksum == 0 && header.page_type == PageType::Allocated {
112 continue;
113 }
114
115 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
132fn 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
197fn 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
230fn 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 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 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 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 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 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 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
393fn 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
455fn 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
488fn 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
525fn 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 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 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
566fn 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 if type_name == filter_upper {
576 return true;
577 }
578
579 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}