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#[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 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 if !opts.show_empty && header.checksum == 0 && header.page_type == PageType::Allocated {
80 continue;
81 }
82
83 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
100fn 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
167fn 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
195fn 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 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 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 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 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 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 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
343fn 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
397fn 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
420fn 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
449fn 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 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 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
481fn 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 if type_name == filter_upper {
491 return true;
492 }
493
494 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}