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