1mod base_wins;
19mod conflict_file;
20mod fork_on_conflict;
21mod last_write_wins;
22mod types;
23
24pub use base_wins::BaseWins;
25pub use conflict_file::ConflictFile;
26pub use fork_on_conflict::ForkOnConflict;
27pub use last_write_wins::LastWriteWins;
28pub use types::{Conflict, MergeResult, Resolution, ResolvedConflict};
29
30use std::path::PathBuf;
31
32use crate::crypto::PublicKey;
33
34use super::path_ops::{OpType, PathOperation};
35
36pub trait ConflictResolver: std::fmt::Debug + Send + Sync {
41 fn resolve(&self, conflict: &Conflict, local_peer: &PublicKey) -> Resolution;
52}
53
54pub fn operations_conflict(base: &PathOperation, incoming: &PathOperation) -> bool {
61 if base.id == incoming.id {
63 return false;
64 }
65
66 if base.path != incoming.path {
68 return false;
69 }
70
71 let base_destructive = is_destructive(&base.op_type);
73 let incoming_destructive = is_destructive(&incoming.op_type);
74
75 base_destructive
77 || incoming_destructive
78 || (matches!(base.op_type, OpType::Add) && matches!(incoming.op_type, OpType::Add))
79}
80
81fn is_destructive(op_type: &OpType) -> bool {
83 matches!(op_type, OpType::Remove | OpType::Mv { .. })
84}
85
86pub fn conflicts_with_mv_source(op: &PathOperation, mv_from: &PathBuf) -> bool {
91 &op.path == mv_from
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98 use crate::crypto::SecretKey;
99 use crate::mount::path_ops::OpId;
100
101 fn make_peer_id(seed: u8) -> PublicKey {
102 let mut seed_bytes = [0u8; 32];
103 seed_bytes[0] = seed;
104 let secret = SecretKey::from(seed_bytes);
105 secret.public()
106 }
107
108 fn make_op(peer_id: PublicKey, timestamp: u64, op_type: OpType, path: &str) -> PathOperation {
109 PathOperation {
110 id: OpId { timestamp, peer_id },
111 op_type,
112 path: PathBuf::from(path),
113 content_link: None,
114 is_dir: false,
115 }
116 }
117
118 #[test]
119 fn test_conflict_detection() {
120 let peer1 = make_peer_id(1);
121 let peer2 = make_peer_id(2);
122
123 let op1 = make_op(peer1, 1, OpType::Add, "file.txt");
124 let op2 = make_op(peer2, 1, OpType::Add, "file.txt");
125
126 assert!(operations_conflict(&op1, &op2));
128 }
129
130 #[test]
131 fn test_no_conflict_different_paths() {
132 let peer1 = make_peer_id(1);
133 let peer2 = make_peer_id(2);
134
135 let op1 = make_op(peer1, 1, OpType::Add, "file1.txt");
136 let op2 = make_op(peer2, 1, OpType::Add, "file2.txt");
137
138 assert!(!operations_conflict(&op1, &op2));
140 }
141
142 #[test]
143 fn test_no_conflict_same_operation() {
144 let peer1 = make_peer_id(1);
145
146 let op1 = make_op(peer1, 1, OpType::Add, "file.txt");
147 let op2 = op1.clone();
148
149 assert!(!operations_conflict(&op1, &op2));
151 }
152
153 #[test]
154 fn test_conflict_add_vs_remove() {
155 let peer1 = make_peer_id(1);
156 let peer2 = make_peer_id(2);
157
158 let op1 = make_op(peer1, 1, OpType::Add, "file.txt");
159 let op2 = make_op(peer2, 1, OpType::Remove, "file.txt");
160
161 assert!(operations_conflict(&op1, &op2));
163 }
164
165 #[test]
166 fn test_conflict_mkdir_vs_remove() {
167 let peer1 = make_peer_id(1);
168 let peer2 = make_peer_id(2);
169
170 let op1 = make_op(peer1, 1, OpType::Mkdir, "dir");
171 let op2 = make_op(peer2, 1, OpType::Remove, "dir");
172
173 assert!(operations_conflict(&op1, &op2));
175 }
176
177 #[test]
178 fn test_no_conflict_mkdir_vs_mkdir() {
179 let peer1 = make_peer_id(1);
180 let peer2 = make_peer_id(2);
181
182 let op1 = make_op(peer1, 1, OpType::Mkdir, "dir");
183 let op2 = make_op(peer2, 1, OpType::Mkdir, "dir");
184
185 assert!(!operations_conflict(&op1, &op2));
187 }
188
189 #[test]
190 fn test_conflict_is_concurrent() {
191 let peer1 = make_peer_id(1);
192 let peer2 = make_peer_id(2);
193
194 let base = make_op(peer1, 5, OpType::Add, "file.txt");
195 let incoming = make_op(peer2, 5, OpType::Remove, "file.txt");
196
197 let conflict = Conflict::new(PathBuf::from("file.txt"), base, incoming);
198
199 assert!(conflict.is_concurrent());
201 }
202
203 #[test]
204 fn test_conflict_not_concurrent() {
205 let peer1 = make_peer_id(1);
206 let peer2 = make_peer_id(2);
207
208 let base = make_op(peer1, 3, OpType::Add, "file.txt");
209 let incoming = make_op(peer2, 5, OpType::Remove, "file.txt");
210
211 let conflict = Conflict::new(PathBuf::from("file.txt"), base, incoming);
212
213 assert!(!conflict.is_concurrent());
215 }
216
217 #[test]
218 fn test_crdt_winner() {
219 let peer1 = make_peer_id(1);
220 let peer2 = make_peer_id(2);
221
222 let base = make_op(peer1, 3, OpType::Add, "file.txt");
223 let incoming = make_op(peer2, 5, OpType::Remove, "file.txt");
224
225 let conflict = Conflict::new(PathBuf::from("file.txt"), base, incoming.clone());
226
227 assert_eq!(conflict.crdt_winner().id, incoming.id);
229 }
230
231 #[test]
232 fn test_merge_result() {
233 let mut result = MergeResult::new();
234
235 assert_eq!(result.operations_added, 0);
236 assert!(!result.has_unresolved());
237 assert_eq!(result.total_conflicts(), 0);
238
239 let peer1 = make_peer_id(1);
241 let peer2 = make_peer_id(2);
242 let base = make_op(peer1, 1, OpType::Add, "file.txt");
243 let incoming = make_op(peer2, 1, OpType::Add, "file.txt");
244
245 result
246 .unresolved_conflicts
247 .push(Conflict::new(PathBuf::from("file.txt"), base, incoming));
248
249 assert!(result.has_unresolved());
250 assert_eq!(result.total_conflicts(), 1);
251 }
252}