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