1use std::io::Write;
2
3use colored::Colorize;
4use serde::Serialize;
5
6use crate::cli::{create_progress_bar, wprintln};
7use crate::innodb::constants::SIZE_FIL_HEAD;
8use crate::innodb::page::FilHeader;
9use crate::innodb::tablespace::Tablespace;
10use crate::IdbError;
11
12pub struct DiffOptions {
14 pub file1: String,
16 pub file2: String,
18 pub verbose: bool,
20 pub byte_ranges: bool,
22 pub page: Option<u64>,
24 pub json: bool,
26 pub page_size: Option<u32>,
28 pub keyring: Option<String>,
30}
31
32#[derive(Serialize)]
35struct DiffReport {
36 file1: FileInfo,
37 file2: FileInfo,
38 page_size_mismatch: bool,
39 summary: DiffSummary,
40 #[serde(skip_serializing_if = "Vec::is_empty")]
41 modified_pages: Vec<PageDiff>,
42}
43
44#[derive(Serialize)]
45struct FileInfo {
46 path: String,
47 page_count: u64,
48 page_size: u32,
49}
50
51#[derive(Serialize)]
52struct DiffSummary {
53 identical: u64,
54 modified: u64,
55 only_in_file1: u64,
56 only_in_file2: u64,
57}
58
59#[derive(Serialize)]
60struct PageDiff {
61 page_number: u64,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 file1_header: Option<HeaderFields>,
64 #[serde(skip_serializing_if = "Option::is_none")]
65 file2_header: Option<HeaderFields>,
66 #[serde(skip_serializing_if = "Vec::is_empty")]
67 changed_fields: Vec<FieldChange>,
68 #[serde(skip_serializing_if = "Vec::is_empty")]
69 byte_ranges: Vec<ByteRange>,
70 #[serde(skip_serializing_if = "Option::is_none")]
71 total_bytes_changed: Option<usize>,
72}
73
74#[derive(Serialize)]
75struct HeaderFields {
76 checksum: String,
77 page_number: u32,
78 prev_page: String,
79 next_page: String,
80 lsn: u64,
81 page_type: String,
82 flush_lsn: u64,
83 space_id: u32,
84}
85
86#[derive(Serialize)]
87struct FieldChange {
88 field: String,
89 old_value: String,
90 new_value: String,
91}
92
93#[derive(Serialize)]
94struct ByteRange {
95 start: usize,
96 end: usize,
97 length: usize,
98}
99
100fn header_to_fields(h: &FilHeader) -> HeaderFields {
103 HeaderFields {
104 checksum: format!("0x{:08X}", h.checksum),
105 page_number: h.page_number,
106 prev_page: format!("0x{:08X}", h.prev_page),
107 next_page: format!("0x{:08X}", h.next_page),
108 lsn: h.lsn,
109 page_type: h.page_type.name().to_string(),
110 flush_lsn: h.flush_lsn,
111 space_id: h.space_id,
112 }
113}
114
115fn compare_headers(h1: &FilHeader, h2: &FilHeader) -> Vec<FieldChange> {
116 let mut changes = Vec::new();
117
118 if h1.checksum != h2.checksum {
119 changes.push(FieldChange {
120 field: "Checksum".to_string(),
121 old_value: format!("0x{:08X}", h1.checksum),
122 new_value: format!("0x{:08X}", h2.checksum),
123 });
124 }
125 if h1.page_number != h2.page_number {
126 changes.push(FieldChange {
127 field: "Page Number".to_string(),
128 old_value: h1.page_number.to_string(),
129 new_value: h2.page_number.to_string(),
130 });
131 }
132 if h1.prev_page != h2.prev_page {
133 changes.push(FieldChange {
134 field: "Prev Page".to_string(),
135 old_value: format!("0x{:08X}", h1.prev_page),
136 new_value: format!("0x{:08X}", h2.prev_page),
137 });
138 }
139 if h1.next_page != h2.next_page {
140 changes.push(FieldChange {
141 field: "Next Page".to_string(),
142 old_value: format!("0x{:08X}", h1.next_page),
143 new_value: format!("0x{:08X}", h2.next_page),
144 });
145 }
146 if h1.lsn != h2.lsn {
147 changes.push(FieldChange {
148 field: "LSN".to_string(),
149 old_value: h1.lsn.to_string(),
150 new_value: h2.lsn.to_string(),
151 });
152 }
153 if h1.page_type != h2.page_type {
154 changes.push(FieldChange {
155 field: "Page Type".to_string(),
156 old_value: h1.page_type.name().to_string(),
157 new_value: h2.page_type.name().to_string(),
158 });
159 }
160 if h1.flush_lsn != h2.flush_lsn {
161 changes.push(FieldChange {
162 field: "Flush LSN".to_string(),
163 old_value: h1.flush_lsn.to_string(),
164 new_value: h2.flush_lsn.to_string(),
165 });
166 }
167 if h1.space_id != h2.space_id {
168 changes.push(FieldChange {
169 field: "Space ID".to_string(),
170 old_value: h1.space_id.to_string(),
171 new_value: h2.space_id.to_string(),
172 });
173 }
174
175 changes
176}
177
178fn find_diff_ranges(data1: &[u8], data2: &[u8]) -> Vec<ByteRange> {
179 let len = data1.len().min(data2.len());
180 let mut ranges = Vec::new();
181 let mut in_diff = false;
182 let mut start = 0;
183
184 for i in 0..len {
185 if data1[i] != data2[i] {
186 if !in_diff {
187 in_diff = true;
188 start = i;
189 }
190 } else if in_diff {
191 in_diff = false;
192 ranges.push(ByteRange {
193 start,
194 end: i,
195 length: i - start,
196 });
197 }
198 }
199 if in_diff {
200 ranges.push(ByteRange {
201 start,
202 end: len,
203 length: len - start,
204 });
205 }
206
207 ranges
208}
209
210pub fn execute(opts: &DiffOptions, writer: &mut dyn Write) -> Result<(), IdbError> {
212 let mut ts1 = match opts.page_size {
213 Some(ps) => Tablespace::open_with_page_size(&opts.file1, ps)?,
214 None => Tablespace::open(&opts.file1)?,
215 };
216 let mut ts2 = match opts.page_size {
217 Some(ps) => Tablespace::open_with_page_size(&opts.file2, ps)?,
218 None => Tablespace::open(&opts.file2)?,
219 };
220
221 if let Some(ref keyring_path) = opts.keyring {
222 crate::cli::setup_decryption(&mut ts1, keyring_path)?;
223 crate::cli::setup_decryption(&mut ts2, keyring_path)?;
224 }
225
226 let ps1 = ts1.page_size();
227 let ps2 = ts2.page_size();
228 let pc1 = ts1.page_count();
229 let pc2 = ts2.page_count();
230
231 let page_size_mismatch = ps1 != ps2;
232
233 if opts.json {
234 return execute_json(opts, &mut ts1, &mut ts2, page_size_mismatch, writer);
235 }
236
237 wprintln!(writer, "Comparing:")?;
239 wprintln!(
240 writer,
241 " File 1: {} ({} pages, {} bytes/page)",
242 opts.file1, pc1, ps1
243 )?;
244 wprintln!(
245 writer,
246 " File 2: {} ({} pages, {} bytes/page)",
247 opts.file2, pc2, ps2
248 )?;
249 wprintln!(writer)?;
250
251 if page_size_mismatch {
252 wprintln!(
253 writer,
254 "{}",
255 format!(
256 "WARNING: Page size mismatch ({} vs {}). Comparing FIL headers only.",
257 ps1, ps2
258 )
259 .yellow()
260 )?;
261 wprintln!(writer)?;
262 }
263
264 let (start_page, end_page) = match opts.page {
266 Some(p) => {
267 if p >= pc1 && p >= pc2 {
268 return Err(IdbError::Argument(format!(
269 "Page {} out of range (file1 has {} pages, file2 has {} pages)",
270 p, pc1, pc2
271 )));
272 }
273 (p, p + 1)
274 }
275 None => (0, pc1.max(pc2)),
276 };
277
278 let common_pages = pc1.min(pc2);
279 let mut identical = 0u64;
280 let mut modified = 0u64;
281 let mut only_in_file1 = 0u64;
282 let mut only_in_file2 = 0u64;
283 let mut modified_page_nums: Vec<u64> = Vec::new();
284
285 let total = end_page - start_page;
286 let pb = create_progress_bar(total, "pages");
287
288 for page_num in start_page..end_page {
289 pb.inc(1);
290
291 if page_num >= pc1 {
293 only_in_file2 += 1;
294 continue;
295 }
296 if page_num >= pc2 {
297 only_in_file1 += 1;
298 continue;
299 }
300
301 let data1 = ts1.read_page(page_num)?;
302 let data2 = ts2.read_page(page_num)?;
303
304 if page_size_mismatch {
305 let cmp_len = SIZE_FIL_HEAD.min(data1.len()).min(data2.len());
307 if data1[..cmp_len] == data2[..cmp_len] {
308 identical += 1;
309 } else {
310 modified += 1;
311 modified_page_nums.push(page_num);
312
313 if opts.verbose {
314 print_page_diff(writer, page_num, &data1, &data2, opts.byte_ranges, true)?;
315 }
316 }
317 } else {
318 if data1 == data2 {
320 identical += 1;
321 } else {
322 modified += 1;
323 modified_page_nums.push(page_num);
324
325 if opts.verbose {
326 print_page_diff(writer, page_num, &data1, &data2, opts.byte_ranges, false)?;
327 }
328 }
329 }
330 }
331
332 pb.finish_and_clear();
333
334 if opts.page.is_none() {
336 if pc1 > common_pages {
337 only_in_file1 = pc1 - common_pages;
338 }
339 if pc2 > common_pages {
340 only_in_file2 = pc2 - common_pages;
341 }
342 }
343
344 wprintln!(writer, "Summary:")?;
346 wprintln!(writer, " Identical pages: {}", identical)?;
347 if modified > 0 {
348 wprintln!(
349 writer,
350 " Modified pages: {}",
351 format!("{}", modified).red()
352 )?;
353 } else {
354 wprintln!(writer, " Modified pages: {}", modified)?;
355 }
356 wprintln!(writer, " Only in file 1: {}", only_in_file1)?;
357 wprintln!(writer, " Only in file 2: {}", only_in_file2)?;
358
359 if !modified_page_nums.is_empty() {
360 wprintln!(writer)?;
361 let nums: Vec<String> = modified_page_nums.iter().map(|n| n.to_string()).collect();
362 wprintln!(writer, "Modified pages: {}", nums.join(", "))?;
363 }
364
365 Ok(())
366}
367
368fn print_page_diff(
369 writer: &mut dyn Write,
370 page_num: u64,
371 data1: &[u8],
372 data2: &[u8],
373 show_byte_ranges: bool,
374 header_only: bool,
375) -> Result<(), IdbError> {
376 wprintln!(writer, "Page {}: {}", page_num, "MODIFIED".red())?;
377
378 let h1 = FilHeader::parse(data1);
379 let h2 = FilHeader::parse(data2);
380
381 match (h1, h2) {
382 (Some(h1), Some(h2)) => {
383 let changes = compare_headers(&h1, &h2);
384 if changes.is_empty() {
385 wprintln!(writer, " FIL header: identical (data content differs)")?;
386 } else {
387 for c in &changes {
388 wprintln!(writer, " {}: {} -> {}", c.field, c.old_value, c.new_value)?;
389 }
390 }
391
392 if h1.page_type == h2.page_type
394 && !changes.iter().any(|c| c.field == "Page Type")
395 {
396 wprintln!(writer, " Page Type: {} (unchanged)", h1.page_type.name())?;
397 }
398 }
399 _ => {
400 wprintln!(writer, " (could not parse one or both FIL headers)")?;
401 }
402 }
403
404 if show_byte_ranges && !header_only {
405 let ranges = find_diff_ranges(data1, data2);
406 if !ranges.is_empty() {
407 wprintln!(writer, " Byte diff ranges:")?;
408 for r in &ranges {
409 wprintln!(writer, " {}-{} ({} bytes)", r.start, r.end, r.length)?;
410 }
411 let total_changed: usize = ranges.iter().map(|r| r.length).sum();
412 let page_size = data1.len();
413 let pct = (total_changed as f64 / page_size as f64) * 100.0;
414 wprintln!(
415 writer,
416 " Total: {} bytes changed ({:.1}% of page)",
417 total_changed,
418 pct
419 )?;
420 }
421 }
422
423 wprintln!(writer)?;
424 Ok(())
425}
426
427fn execute_json(
428 opts: &DiffOptions,
429 ts1: &mut Tablespace,
430 ts2: &mut Tablespace,
431 page_size_mismatch: bool,
432 writer: &mut dyn Write,
433) -> Result<(), IdbError> {
434 let ps1 = ts1.page_size();
435 let ps2 = ts2.page_size();
436 let pc1 = ts1.page_count();
437 let pc2 = ts2.page_count();
438
439 let (start_page, end_page) = match opts.page {
440 Some(p) => {
441 if p >= pc1 && p >= pc2 {
442 return Err(IdbError::Argument(format!(
443 "Page {} out of range (file1 has {} pages, file2 has {} pages)",
444 p, pc1, pc2
445 )));
446 }
447 (p, p + 1)
448 }
449 None => (0, pc1.max(pc2)),
450 };
451
452 let mut identical = 0u64;
453 let mut modified = 0u64;
454 let mut only_in_file1 = 0u64;
455 let mut only_in_file2 = 0u64;
456 let mut modified_pages: Vec<PageDiff> = Vec::new();
457
458 for page_num in start_page..end_page {
459 if page_num >= pc1 {
460 only_in_file2 += 1;
461 continue;
462 }
463 if page_num >= pc2 {
464 only_in_file1 += 1;
465 continue;
466 }
467
468 let data1 = ts1.read_page(page_num)?;
469 let data2 = ts2.read_page(page_num)?;
470
471 let is_equal = if page_size_mismatch {
472 let cmp_len = SIZE_FIL_HEAD.min(data1.len()).min(data2.len());
473 data1[..cmp_len] == data2[..cmp_len]
474 } else {
475 data1 == data2
476 };
477
478 if is_equal {
479 identical += 1;
480 } else {
481 modified += 1;
482
483 let h1 = FilHeader::parse(&data1);
484 let h2 = FilHeader::parse(&data2);
485
486 let (file1_header, file2_header, changed_fields) = match (&h1, &h2) {
487 (Some(h1), Some(h2)) => {
488 let changes = compare_headers(h1, h2);
489 (
490 Some(header_to_fields(h1)),
491 Some(header_to_fields(h2)),
492 changes,
493 )
494 }
495 _ => (
496 h1.as_ref().map(header_to_fields),
497 h2.as_ref().map(header_to_fields),
498 Vec::new(),
499 ),
500 };
501
502 let (byte_ranges, total_bytes_changed) =
503 if opts.byte_ranges && !page_size_mismatch {
504 let ranges = find_diff_ranges(&data1, &data2);
505 let total: usize = ranges.iter().map(|r| r.length).sum();
506 (ranges, Some(total))
507 } else {
508 (Vec::new(), None)
509 };
510
511 modified_pages.push(PageDiff {
512 page_number: page_num,
513 file1_header,
514 file2_header,
515 changed_fields,
516 byte_ranges,
517 total_bytes_changed,
518 });
519 }
520 }
521
522 if opts.page.is_none() {
524 let common = pc1.min(pc2);
525 if pc1 > common {
526 only_in_file1 = pc1 - common;
527 }
528 if pc2 > common {
529 only_in_file2 = pc2 - common;
530 }
531 }
532
533 let report = DiffReport {
534 file1: FileInfo {
535 path: opts.file1.clone(),
536 page_count: pc1,
537 page_size: ps1,
538 },
539 file2: FileInfo {
540 path: opts.file2.clone(),
541 page_count: pc2,
542 page_size: ps2,
543 },
544 page_size_mismatch,
545 summary: DiffSummary {
546 identical,
547 modified,
548 only_in_file1,
549 only_in_file2,
550 },
551 modified_pages,
552 };
553
554 let json = serde_json::to_string_pretty(&report)
555 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
556 wprintln!(writer, "{}", json)?;
557
558 Ok(())
559}