mod base_wins;
mod conflict_file;
mod fork_on_conflict;
mod last_write_wins;
mod types;
pub use base_wins::BaseWins;
pub use conflict_file::ConflictFile;
pub use fork_on_conflict::ForkOnConflict;
pub use last_write_wins::LastWriteWins;
pub use types::{Conflict, MergeResult, Resolution, ResolvedConflict};
use std::path::PathBuf;
use crate::crypto::PublicKey;
use super::path_ops::{OpType, PathOperation};
pub trait ConflictResolver: std::fmt::Debug + Send + Sync {
fn resolve(&self, conflict: &Conflict, local_peer: &PublicKey) -> Resolution;
}
pub fn operations_conflict(base: &PathOperation, incoming: &PathOperation) -> bool {
if base.id == incoming.id {
return false;
}
if base.path != incoming.path {
return false;
}
let base_destructive = is_destructive(&base.op_type);
let incoming_destructive = is_destructive(&incoming.op_type);
base_destructive
|| incoming_destructive
|| (matches!(base.op_type, OpType::Add) && matches!(incoming.op_type, OpType::Add))
}
fn is_destructive(op_type: &OpType) -> bool {
matches!(op_type, OpType::Remove | OpType::Mv { .. })
}
pub fn conflicts_with_mv_source(op: &PathOperation, mv_from: &PathBuf) -> bool {
&op.path == mv_from
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::SecretKey;
use crate::mount::path_ops::OpId;
fn make_peer_id(seed: u8) -> PublicKey {
let mut seed_bytes = [0u8; 32];
seed_bytes[0] = seed;
let secret = SecretKey::from(seed_bytes);
secret.public()
}
fn make_op(peer_id: PublicKey, timestamp: u64, op_type: OpType, path: &str) -> PathOperation {
PathOperation {
id: OpId { timestamp, peer_id },
op_type,
path: PathBuf::from(path),
content_link: None,
is_dir: false,
}
}
#[test]
fn test_conflict_detection() {
let peer1 = make_peer_id(1);
let peer2 = make_peer_id(2);
let op1 = make_op(peer1, 1, OpType::Add, "file.txt");
let op2 = make_op(peer2, 1, OpType::Add, "file.txt");
assert!(operations_conflict(&op1, &op2));
}
#[test]
fn test_no_conflict_different_paths() {
let peer1 = make_peer_id(1);
let peer2 = make_peer_id(2);
let op1 = make_op(peer1, 1, OpType::Add, "file1.txt");
let op2 = make_op(peer2, 1, OpType::Add, "file2.txt");
assert!(!operations_conflict(&op1, &op2));
}
#[test]
fn test_no_conflict_same_operation() {
let peer1 = make_peer_id(1);
let op1 = make_op(peer1, 1, OpType::Add, "file.txt");
let op2 = op1.clone();
assert!(!operations_conflict(&op1, &op2));
}
#[test]
fn test_conflict_add_vs_remove() {
let peer1 = make_peer_id(1);
let peer2 = make_peer_id(2);
let op1 = make_op(peer1, 1, OpType::Add, "file.txt");
let op2 = make_op(peer2, 1, OpType::Remove, "file.txt");
assert!(operations_conflict(&op1, &op2));
}
#[test]
fn test_conflict_mkdir_vs_remove() {
let peer1 = make_peer_id(1);
let peer2 = make_peer_id(2);
let op1 = make_op(peer1, 1, OpType::Mkdir, "dir");
let op2 = make_op(peer2, 1, OpType::Remove, "dir");
assert!(operations_conflict(&op1, &op2));
}
#[test]
fn test_no_conflict_mkdir_vs_mkdir() {
let peer1 = make_peer_id(1);
let peer2 = make_peer_id(2);
let op1 = make_op(peer1, 1, OpType::Mkdir, "dir");
let op2 = make_op(peer2, 1, OpType::Mkdir, "dir");
assert!(!operations_conflict(&op1, &op2));
}
#[test]
fn test_conflict_is_concurrent() {
let peer1 = make_peer_id(1);
let peer2 = make_peer_id(2);
let base = make_op(peer1, 5, OpType::Add, "file.txt");
let incoming = make_op(peer2, 5, OpType::Remove, "file.txt");
let conflict = Conflict::new(PathBuf::from("file.txt"), base, incoming);
assert!(conflict.is_concurrent());
}
#[test]
fn test_conflict_not_concurrent() {
let peer1 = make_peer_id(1);
let peer2 = make_peer_id(2);
let base = make_op(peer1, 3, OpType::Add, "file.txt");
let incoming = make_op(peer2, 5, OpType::Remove, "file.txt");
let conflict = Conflict::new(PathBuf::from("file.txt"), base, incoming);
assert!(!conflict.is_concurrent());
}
#[test]
fn test_crdt_winner() {
let peer1 = make_peer_id(1);
let peer2 = make_peer_id(2);
let base = make_op(peer1, 3, OpType::Add, "file.txt");
let incoming = make_op(peer2, 5, OpType::Remove, "file.txt");
let conflict = Conflict::new(PathBuf::from("file.txt"), base, incoming.clone());
assert_eq!(conflict.crdt_winner().id, incoming.id);
}
#[test]
fn test_merge_result() {
let mut result = MergeResult::new();
assert_eq!(result.operations_added, 0);
assert!(!result.has_unresolved());
assert_eq!(result.total_conflicts(), 0);
let peer1 = make_peer_id(1);
let peer2 = make_peer_id(2);
let base = make_op(peer1, 1, OpType::Add, "file.txt");
let incoming = make_op(peer2, 1, OpType::Add, "file.txt");
result
.unresolved_conflicts
.push(Conflict::new(PathBuf::from("file.txt"), base, incoming));
assert!(result.has_unresolved());
assert_eq!(result.total_conflicts(), 1);
}
}