ccsync_core/
comparison.rs

1//! File comparison, diff generation, and conflict detection
2//!
3//! This module provides read-only analysis of files to determine:
4//! - Content differences via SHA-256 hashing
5//! - Which file is newer via timestamp comparison
6//! - Visual diffs for changed files
7//! - Conflict classification and resolution strategy determination
8
9mod diff;
10mod hash;
11mod timestamp;
12
13#[cfg(test)]
14mod integration_tests;
15
16use std::path::Path;
17
18use serde::{Deserialize, Serialize};
19
20pub use diff::DiffGenerator;
21pub use hash::FileHasher;
22pub use timestamp::TimestampComparator;
23
24use crate::error::Result;
25
26/// Conflict resolution strategy
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
28#[serde(rename_all = "kebab-case")]
29pub enum ConflictStrategy {
30    /// Abort on conflict
31    Fail,
32    /// Overwrite destination with source
33    Overwrite,
34    /// Skip conflicting files
35    Skip,
36    /// Keep the newer file based on modification time
37    Newer,
38}
39
40/// Result of comparing two files
41#[derive(Debug, Clone, PartialEq, Eq)]
42pub enum ComparisonResult {
43    /// Files have identical content
44    Identical,
45    /// Only source file exists
46    SourceOnly,
47    /// Only destination file exists
48    DestinationOnly,
49    /// Both files exist with different content (conflict)
50    Conflict {
51        /// Whether source is newer than destination
52        source_newer: bool,
53        /// Chosen resolution strategy
54        strategy: ConflictStrategy,
55    },
56}
57
58/// File comparator that combines hashing, timestamps, and diff generation
59pub struct FileComparator;
60
61impl Default for FileComparator {
62    fn default() -> Self {
63        Self::new()
64    }
65}
66
67impl FileComparator {
68    /// Create a new file comparator
69    #[must_use]
70    pub const fn new() -> Self {
71        Self
72    }
73
74    /// Compare two file paths and determine the comparison result
75    ///
76    /// # Errors
77    ///
78    /// Returns an error if file I/O operations fail.
79    pub fn compare(
80        source: &Path,
81        destination: &Path,
82        strategy: ConflictStrategy,
83    ) -> Result<ComparisonResult> {
84        let source_exists = source.exists();
85        let dest_exists = destination.exists();
86
87        match (source_exists, dest_exists) {
88            (false, false) => {
89                anyhow::bail!(
90                    "Neither source nor destination file exists: source={}, dest={}",
91                    source.display(),
92                    destination.display()
93                )
94            }
95            (true, false) => Ok(ComparisonResult::SourceOnly),
96            (false, true) => Ok(ComparisonResult::DestinationOnly),
97            (true, true) => {
98                // Both exist - check if content differs
99                let source_hash = FileHasher::hash(source)?;
100                let dest_hash = FileHasher::hash(destination)?;
101
102                if source_hash == dest_hash {
103                    Ok(ComparisonResult::Identical)
104                } else {
105                    // Conflict - both exist with different content
106                    let source_newer = TimestampComparator::is_newer(source, destination)?;
107                    Ok(ComparisonResult::Conflict {
108                        source_newer,
109                        strategy,
110                    })
111                }
112            }
113        }
114    }
115
116    /// Generate a colored diff between two files
117    ///
118    /// # Errors
119    ///
120    /// Returns an error if file reading fails.
121    pub fn generate_diff(source: &Path, destination: &Path) -> Result<String> {
122        DiffGenerator::generate(source, destination)
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn test_conflict_strategy_types() {
132        assert_eq!(ConflictStrategy::Fail, ConflictStrategy::Fail);
133        assert_ne!(ConflictStrategy::Fail, ConflictStrategy::Overwrite);
134    }
135
136    #[test]
137    fn test_comparison_result_types() {
138        let identical = ComparisonResult::Identical;
139        let source_only = ComparisonResult::SourceOnly;
140        assert_ne!(identical, source_only);
141    }
142}