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,
243 pc1,
244 ps1
245 )?;
246 wprintln!(
247 writer,
248 " File 2: {} ({} pages, {} bytes/page)",
249 opts.file2,
250 pc2,
251 ps2
252 )?;
253 wprintln!(writer)?;
254
255 if page_size_mismatch {
256 wprintln!(
257 writer,
258 "{}",
259 format!(
260 "WARNING: Page size mismatch ({} vs {}). Comparing FIL headers only.",
261 ps1, ps2
262 )
263 .yellow()
264 )?;
265 wprintln!(writer)?;
266 }
267
268 let (start_page, end_page) = match opts.page {
270 Some(p) => {
271 if p >= pc1 && p >= pc2 {
272 return Err(IdbError::Argument(format!(
273 "Page {} out of range (file1 has {} pages, file2 has {} pages)",
274 p, pc1, pc2
275 )));
276 }
277 (p, p + 1)
278 }
279 None => (0, pc1.max(pc2)),
280 };
281
282 let common_pages = pc1.min(pc2);
283 let mut identical = 0u64;
284 let mut modified = 0u64;
285 let mut only_in_file1 = 0u64;
286 let mut only_in_file2 = 0u64;
287 let mut modified_page_nums: Vec<u64> = Vec::new();
288
289 let total = end_page - start_page;
290 let pb = create_progress_bar(total, "pages");
291
292 for page_num in start_page..end_page {
293 pb.inc(1);
294
295 if page_num >= pc1 {
297 only_in_file2 += 1;
298 continue;
299 }
300 if page_num >= pc2 {
301 only_in_file1 += 1;
302 continue;
303 }
304
305 let data1 = ts1.read_page(page_num)?;
306 let data2 = ts2.read_page(page_num)?;
307
308 if page_size_mismatch {
309 let cmp_len = SIZE_FIL_HEAD.min(data1.len()).min(data2.len());
311 if data1[..cmp_len] == data2[..cmp_len] {
312 identical += 1;
313 } else {
314 modified += 1;
315 modified_page_nums.push(page_num);
316
317 if opts.verbose {
318 print_page_diff(writer, page_num, &data1, &data2, opts.byte_ranges, true)?;
319 }
320 }
321 } else {
322 if data1 == data2 {
324 identical += 1;
325 } else {
326 modified += 1;
327 modified_page_nums.push(page_num);
328
329 if opts.verbose {
330 print_page_diff(writer, page_num, &data1, &data2, opts.byte_ranges, false)?;
331 }
332 }
333 }
334 }
335
336 pb.finish_and_clear();
337
338 if opts.page.is_none() {
340 if pc1 > common_pages {
341 only_in_file1 = pc1 - common_pages;
342 }
343 if pc2 > common_pages {
344 only_in_file2 = pc2 - common_pages;
345 }
346 }
347
348 wprintln!(writer, "Summary:")?;
350 wprintln!(writer, " Identical pages: {}", identical)?;
351 if modified > 0 {
352 wprintln!(
353 writer,
354 " Modified pages: {}",
355 format!("{}", modified).red()
356 )?;
357 } else {
358 wprintln!(writer, " Modified pages: {}", modified)?;
359 }
360 wprintln!(writer, " Only in file 1: {}", only_in_file1)?;
361 wprintln!(writer, " Only in file 2: {}", only_in_file2)?;
362
363 if !modified_page_nums.is_empty() {
364 wprintln!(writer)?;
365 let nums: Vec<String> = modified_page_nums.iter().map(|n| n.to_string()).collect();
366 wprintln!(writer, "Modified pages: {}", nums.join(", "))?;
367 }
368
369 Ok(())
370}
371
372fn print_page_diff(
373 writer: &mut dyn Write,
374 page_num: u64,
375 data1: &[u8],
376 data2: &[u8],
377 show_byte_ranges: bool,
378 header_only: bool,
379) -> Result<(), IdbError> {
380 wprintln!(writer, "Page {}: {}", page_num, "MODIFIED".red())?;
381
382 let h1 = FilHeader::parse(data1);
383 let h2 = FilHeader::parse(data2);
384
385 match (h1, h2) {
386 (Some(h1), Some(h2)) => {
387 let changes = compare_headers(&h1, &h2);
388 if changes.is_empty() {
389 wprintln!(writer, " FIL header: identical (data content differs)")?;
390 } else {
391 for c in &changes {
392 wprintln!(writer, " {}: {} -> {}", c.field, c.old_value, c.new_value)?;
393 }
394 }
395
396 if h1.page_type == h2.page_type && !changes.iter().any(|c| c.field == "Page Type") {
398 wprintln!(writer, " Page Type: {} (unchanged)", h1.page_type.name())?;
399 }
400 }
401 _ => {
402 wprintln!(writer, " (could not parse one or both FIL headers)")?;
403 }
404 }
405
406 if show_byte_ranges && !header_only {
407 let ranges = find_diff_ranges(data1, data2);
408 if !ranges.is_empty() {
409 wprintln!(writer, " Byte diff ranges:")?;
410 for r in &ranges {
411 wprintln!(writer, " {}-{} ({} bytes)", r.start, r.end, r.length)?;
412 }
413 let total_changed: usize = ranges.iter().map(|r| r.length).sum();
414 let page_size = data1.len();
415 let pct = (total_changed as f64 / page_size as f64) * 100.0;
416 wprintln!(
417 writer,
418 " Total: {} bytes changed ({:.1}% of page)",
419 total_changed,
420 pct
421 )?;
422 }
423 }
424
425 wprintln!(writer)?;
426 Ok(())
427}
428
429fn execute_json(
430 opts: &DiffOptions,
431 ts1: &mut Tablespace,
432 ts2: &mut Tablespace,
433 page_size_mismatch: bool,
434 writer: &mut dyn Write,
435) -> Result<(), IdbError> {
436 let ps1 = ts1.page_size();
437 let ps2 = ts2.page_size();
438 let pc1 = ts1.page_count();
439 let pc2 = ts2.page_count();
440
441 let (start_page, end_page) = match opts.page {
442 Some(p) => {
443 if p >= pc1 && p >= pc2 {
444 return Err(IdbError::Argument(format!(
445 "Page {} out of range (file1 has {} pages, file2 has {} pages)",
446 p, pc1, pc2
447 )));
448 }
449 (p, p + 1)
450 }
451 None => (0, pc1.max(pc2)),
452 };
453
454 let mut identical = 0u64;
455 let mut modified = 0u64;
456 let mut only_in_file1 = 0u64;
457 let mut only_in_file2 = 0u64;
458 let mut modified_pages: Vec<PageDiff> = Vec::new();
459
460 for page_num in start_page..end_page {
461 if page_num >= pc1 {
462 only_in_file2 += 1;
463 continue;
464 }
465 if page_num >= pc2 {
466 only_in_file1 += 1;
467 continue;
468 }
469
470 let data1 = ts1.read_page(page_num)?;
471 let data2 = ts2.read_page(page_num)?;
472
473 let is_equal = if page_size_mismatch {
474 let cmp_len = SIZE_FIL_HEAD.min(data1.len()).min(data2.len());
475 data1[..cmp_len] == data2[..cmp_len]
476 } else {
477 data1 == data2
478 };
479
480 if is_equal {
481 identical += 1;
482 } else {
483 modified += 1;
484
485 let h1 = FilHeader::parse(&data1);
486 let h2 = FilHeader::parse(&data2);
487
488 let (file1_header, file2_header, changed_fields) = match (&h1, &h2) {
489 (Some(h1), Some(h2)) => {
490 let changes = compare_headers(h1, h2);
491 (
492 Some(header_to_fields(h1)),
493 Some(header_to_fields(h2)),
494 changes,
495 )
496 }
497 _ => (
498 h1.as_ref().map(header_to_fields),
499 h2.as_ref().map(header_to_fields),
500 Vec::new(),
501 ),
502 };
503
504 let (byte_ranges, total_bytes_changed) = if opts.byte_ranges && !page_size_mismatch {
505 let ranges = find_diff_ranges(&data1, &data2);
506 let total: usize = ranges.iter().map(|r| r.length).sum();
507 (ranges, Some(total))
508 } else {
509 (Vec::new(), None)
510 };
511
512 modified_pages.push(PageDiff {
513 page_number: page_num,
514 file1_header,
515 file2_header,
516 changed_fields,
517 byte_ranges,
518 total_bytes_changed,
519 });
520 }
521 }
522
523 if opts.page.is_none() {
525 let common = pc1.min(pc2);
526 if pc1 > common {
527 only_in_file1 = pc1 - common;
528 }
529 if pc2 > common {
530 only_in_file2 = pc2 - common;
531 }
532 }
533
534 let report = DiffReport {
535 file1: FileInfo {
536 path: opts.file1.clone(),
537 page_count: pc1,
538 page_size: ps1,
539 },
540 file2: FileInfo {
541 path: opts.file2.clone(),
542 page_count: pc2,
543 page_size: ps2,
544 },
545 page_size_mismatch,
546 summary: DiffSummary {
547 identical,
548 modified,
549 only_in_file1,
550 only_in_file2,
551 },
552 modified_pages,
553 };
554
555 let json = serde_json::to_string_pretty(&report)
556 .map_err(|e| IdbError::Parse(format!("JSON serialization error: {}", e)))?;
557 wprintln!(writer, "{}", json)?;
558
559 Ok(())
560}