1use crate::{Archive, FormatVersion, Result};
7use std::collections::{HashMap, HashSet};
8use std::path::Path;
9
10#[derive(Debug, Clone)]
12pub struct CompareOptions {
13 pub detailed: bool,
15 pub content_check: bool,
17 pub metadata_only: bool,
19 pub ignore_order: bool,
21 pub filter: Option<String>,
23}
24
25impl Default for CompareOptions {
26 fn default() -> Self {
27 Self {
28 detailed: false,
29 content_check: false,
30 metadata_only: false,
31 ignore_order: true,
32 filter: None,
33 }
34 }
35}
36
37#[derive(Debug, Clone)]
39pub struct ComparisonResult {
40 pub identical: bool,
42 pub metadata: MetadataComparison,
44 pub files: Option<FileComparison>,
46 pub summary: ComparisonSummary,
48}
49
50#[derive(Debug, Clone)]
52pub struct MetadataComparison {
53 pub format_version: (FormatVersion, FormatVersion),
55 pub block_size: (u16, u16),
57 pub file_count: (usize, usize),
59 pub archive_size: (u64, u64),
61 pub matches: bool,
63}
64
65#[derive(Debug, Clone)]
67pub struct FileComparison {
68 pub source_only: Vec<String>,
70 pub target_only: Vec<String>,
72 pub common_files: Vec<String>,
74 pub size_differences: Vec<FileSizeDiff>,
76 pub content_differences: Vec<String>,
78 pub metadata_differences: Vec<FileMetadataDiff>,
80}
81
82#[derive(Debug, Clone)]
84pub struct FileSizeDiff {
85 pub name: String,
87 pub source_size: u64,
89 pub target_size: u64,
91 pub source_compressed: u64,
93 pub target_compressed: u64,
95}
96
97#[derive(Debug, Clone)]
99pub struct FileMetadataDiff {
100 pub name: String,
102 pub difference: String,
104 pub source_value: String,
106 pub target_value: String,
108}
109
110#[derive(Debug, Clone)]
112pub struct ComparisonSummary {
113 pub source_files: usize,
115 pub target_files: usize,
117 pub source_only_count: usize,
119 pub target_only_count: usize,
121 pub different_files: usize,
123 pub identical_files: usize,
125}
126
127pub fn compare_archives<P: AsRef<Path>>(
129 source_path: P,
130 target_path: P,
131 detailed: bool,
132 content_check: bool,
133 metadata_only: bool,
134 _ignore_order: bool,
135 filter: Option<String>,
136) -> Result<ComparisonResult> {
137 let source_path = source_path.as_ref();
138 let target_path = target_path.as_ref();
139
140 log::info!(
141 "Comparing archives: {} vs {}",
142 source_path.display(),
143 target_path.display()
144 );
145
146 let mut source_archive = Archive::open(source_path)?;
148 let mut target_archive = Archive::open(target_path)?;
149
150 let metadata = compare_metadata(&mut source_archive, &mut target_archive)?;
152
153 if metadata_only {
155 return Ok(ComparisonResult {
156 identical: metadata.matches,
157 metadata: metadata.clone(),
158 files: None,
159 summary: ComparisonSummary {
160 source_files: metadata.file_count.0,
161 target_files: metadata.file_count.1,
162 source_only_count: 0,
163 target_only_count: 0,
164 different_files: 0,
165 identical_files: 0,
166 },
167 });
168 }
169
170 let files = compare_files(
172 &mut source_archive,
173 &mut target_archive,
174 detailed,
175 content_check,
176 filter,
177 )?;
178
179 let summary = ComparisonSummary {
181 source_files: metadata.file_count.0,
182 target_files: metadata.file_count.1,
183 source_only_count: files.source_only.len(),
184 target_only_count: files.target_only.len(),
185 different_files: files.size_differences.len()
186 + files.content_differences.len()
187 + files.metadata_differences.len(),
188 identical_files: files.common_files.len()
189 - files.size_differences.len()
190 - files.content_differences.len()
191 - files.metadata_differences.len(),
192 };
193
194 let files_identical = files.source_only.is_empty()
196 && files.target_only.is_empty()
197 && files.size_differences.is_empty()
198 && files.content_differences.is_empty()
199 && files.metadata_differences.is_empty();
200
201 let identical = metadata.matches && files_identical;
202
203 Ok(ComparisonResult {
204 identical,
205 metadata,
206 files: Some(files),
207 summary,
208 })
209}
210
211fn compare_metadata(source: &mut Archive, target: &mut Archive) -> Result<MetadataComparison> {
213 let source_info = source.get_info()?;
215 let target_info = target.get_info()?;
216
217 let source_header = source.header();
219 let target_header = target.header();
220
221 let format_version = (source_header.format_version, target_header.format_version);
222 let block_size = (source_header.block_size, target_header.block_size);
223 let file_count = (source_info.file_count, target_info.file_count);
224 let archive_size = (source_info.file_size, target_info.file_size);
225
226 let matches = format_version.0 == format_version.1
227 && block_size.0 == block_size.1
228 && file_count.0 == file_count.1;
229
230 Ok(MetadataComparison {
231 format_version,
232 block_size,
233 file_count,
234 archive_size,
235 matches,
236 })
237}
238
239fn compare_files(
241 source: &mut Archive,
242 target: &mut Archive,
243 detailed: bool,
244 content_check: bool,
245 filter: Option<String>,
246) -> Result<FileComparison> {
247 let source_files = get_file_list(source, &filter)?;
249 let target_files = get_file_list(target, &filter)?;
250
251 let source_set: HashSet<_> = source_files.keys().collect();
253 let target_set: HashSet<_> = target_files.keys().collect();
254
255 let source_only: Vec<String> = source_set
257 .difference(&target_set)
258 .map(|s| s.to_string())
259 .collect();
260
261 let target_only: Vec<String> = target_set
262 .difference(&source_set)
263 .map(|s| s.to_string())
264 .collect();
265
266 let common_files: Vec<String> = source_set
267 .intersection(&target_set)
268 .map(|s| s.to_string())
269 .collect();
270
271 let mut size_differences = Vec::new();
273 let mut content_differences = Vec::new();
274 let mut metadata_differences = Vec::new();
275
276 for filename in &common_files {
277 let source_entry = &source_files[filename];
278 let target_entry = &target_files[filename];
279
280 if source_entry.size != target_entry.size
282 || source_entry.compressed_size != target_entry.compressed_size
283 {
284 size_differences.push(FileSizeDiff {
285 name: filename.clone(),
286 source_size: source_entry.size,
287 target_size: target_entry.size,
288 source_compressed: source_entry.compressed_size,
289 target_compressed: target_entry.compressed_size,
290 });
291 }
292
293 if detailed && source_entry.flags != target_entry.flags {
295 metadata_differences.push(FileMetadataDiff {
296 name: filename.clone(),
297 difference: "Flags".to_string(),
298 source_value: format!("0x{:08x}", source_entry.flags),
299 target_value: format!("0x{:08x}", target_entry.flags),
300 });
301 }
302
303 if content_check {
305 match (source.read_file(filename), target.read_file(filename)) {
306 (Ok(source_data), Ok(target_data)) => {
307 if source_data != target_data {
308 content_differences.push(filename.clone());
309 }
310 }
311 _ => {
312 content_differences.push(filename.clone());
314 }
315 }
316 }
317 }
318
319 Ok(FileComparison {
320 source_only,
321 target_only,
322 common_files,
323 size_differences,
324 content_differences,
325 metadata_differences,
326 })
327}
328
329fn get_file_list(
331 archive: &mut Archive,
332 filter: &Option<String>,
333) -> Result<HashMap<String, crate::FileEntry>> {
334 let files = archive
335 .list()
336 .unwrap_or_else(|_| archive.list_all().unwrap_or_default());
337
338 let mut file_map = HashMap::new();
339
340 for file in files {
341 if let Some(pattern) = filter
343 && !simple_pattern_match(&file.name, pattern)
344 {
345 continue;
346 }
347
348 file_map.insert(file.name.clone(), file);
349 }
350
351 Ok(file_map)
352}
353
354fn simple_pattern_match(text: &str, pattern: &str) -> bool {
356 if pattern == "*" {
357 return true;
358 }
359
360 if pattern.contains('*') {
361 let parts: Vec<&str> = pattern.split('*').collect();
363 if parts.is_empty() {
364 return true;
365 }
366
367 let mut text_pos = 0;
368 for (i, part) in parts.iter().enumerate() {
369 if part.is_empty() {
370 continue;
371 }
372
373 if i == 0 {
374 if !text[text_pos..].starts_with(part) {
376 return false;
377 }
378 text_pos += part.len();
379 } else if i == parts.len() - 1 {
380 return text[text_pos..].ends_with(part);
382 } else {
383 if let Some(pos) = text[text_pos..].find(part) {
385 text_pos += pos + part.len();
386 } else {
387 return false;
388 }
389 }
390 }
391 true
392 } else {
393 text == pattern
395 }
396}
397
398#[cfg(test)]
399mod tests {
400 use super::*;
401
402 #[test]
403 fn test_compare_options_default() {
404 let options = CompareOptions::default();
405 assert!(!options.detailed);
406 assert!(!options.content_check);
407 assert!(!options.metadata_only);
408 assert!(options.ignore_order);
409 assert!(options.filter.is_none());
410 }
411
412 #[test]
413 fn test_comparison_summary_new() {
414 let summary = ComparisonSummary {
415 source_files: 100,
416 target_files: 95,
417 source_only_count: 5,
418 target_only_count: 2,
419 different_files: 3,
420 identical_files: 90,
421 };
422
423 assert_eq!(summary.source_files, 100);
424 assert_eq!(summary.target_files, 95);
425 assert_eq!(summary.source_only_count, 5);
426 assert_eq!(summary.target_only_count, 2);
427 assert_eq!(summary.different_files, 3);
428 assert_eq!(summary.identical_files, 90);
429 }
430
431 #[test]
432 fn test_simple_pattern_match() {
433 assert!(simple_pattern_match("test.txt", "test.txt"));
435 assert!(!simple_pattern_match("test.txt", "other.txt"));
436
437 assert!(simple_pattern_match("test.txt", "*"));
439 assert!(simple_pattern_match("test.txt", "*.txt"));
440 assert!(simple_pattern_match("test.txt", "test.*"));
441 assert!(simple_pattern_match("test.txt", "*test*"));
442
443 assert!(simple_pattern_match("folder/test.txt", "*/test.txt"));
445 assert!(simple_pattern_match("folder/test.txt", "folder/*.txt"));
446 assert!(!simple_pattern_match("folder/test.txt", "*.dbc"));
447 assert!(!simple_pattern_match("folder/test.txt", "other/*"));
448 }
449}