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