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