1use std::collections::HashSet;
8use std::fmt;
9
10fn fnv1a_64(data: &[u8]) -> u64 {
16 const FNV_OFFSET_BASIS: u64 = 14_695_981_039_346_656_037;
17 const FNV_PRIME: u64 = 1_099_511_628_211;
18
19 let mut hash = FNV_OFFSET_BASIS;
20 for &byte in data {
21 hash ^= u64::from(byte);
22 hash = hash.wrapping_mul(FNV_PRIME);
23 }
24 hash
25}
26
27fn fingerprint_rules(rules: &[String]) -> u64 {
33 let mut sorted: Vec<&str> = rules.iter().map(String::as_str).collect();
34 sorted.sort_unstable();
35
36 let mut combined: Vec<u8> = Vec::new();
38 for rule in sorted {
39 combined.extend_from_slice(rule.as_bytes());
40 combined.push(0u8);
41 }
42 fnv1a_64(&combined)
43}
44
45fn unix_millis_now() -> u64 {
49 use std::time::{SystemTime, UNIX_EPOCH};
50 SystemTime::now()
51 .duration_since(UNIX_EPOCH)
52 .map(|d| d.as_millis() as u64)
53 .unwrap_or(0)
54}
55
56#[derive(Debug, Clone, PartialEq, Eq)]
65pub struct RuleSetVersion {
66 pub major: u32,
67 pub minor: u32,
68 pub patch: u32,
69 pub fingerprint: u64,
71 pub created_at: u64,
73 pub author_peer_id: String,
75}
76
77impl RuleSetVersion {
78 pub fn new(major: u32, minor: u32, patch: u32, rules: &[String], author: &str) -> Self {
83 Self {
84 major,
85 minor,
86 patch,
87 fingerprint: fingerprint_rules(rules),
88 created_at: unix_millis_now(),
89 author_peer_id: author.to_string(),
90 }
91 }
92
93 pub fn is_compatible_with(&self, other: &Self) -> bool {
96 self.major == other.major
97 }
98}
99
100impl fmt::Display for RuleSetVersion {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103 write!(
104 f,
105 "{}.{}.{}+{:016x}",
106 self.major, self.minor, self.patch, self.fingerprint
107 )
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
117pub struct RuleSetDiff {
118 pub added: Vec<String>,
120 pub removed: Vec<String>,
122 pub unchanged: usize,
124}
125
126impl RuleSetDiff {
127 pub fn diff(old_rules: &[String], new_rules: &[String]) -> Self {
131 let old_set: HashSet<&str> = old_rules.iter().map(String::as_str).collect();
132 let new_set: HashSet<&str> = new_rules.iter().map(String::as_str).collect();
133
134 let mut added: Vec<String> = new_set
135 .difference(&old_set)
136 .map(|s| (*s).to_string())
137 .collect();
138 let mut removed: Vec<String> = old_set
139 .difference(&new_set)
140 .map(|s| (*s).to_string())
141 .collect();
142
143 added.sort_unstable();
144 removed.sort_unstable();
145
146 let unchanged = old_set.intersection(&new_set).count();
147
148 Self {
149 added,
150 removed,
151 unchanged,
152 }
153 }
154
155 pub fn is_empty(&self) -> bool {
157 self.added.is_empty() && self.removed.is_empty()
158 }
159
160 pub fn summary(&self) -> String {
162 format!(
163 "+{}/-{}/={}",
164 self.added.len(),
165 self.removed.len(),
166 self.unchanged
167 )
168 }
169}
170
171impl fmt::Display for RuleSetDiff {
172 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173 write!(f, "{}", self.summary())
174 }
175}
176
177#[derive(Debug, Clone, PartialEq, Eq)]
183pub enum ConflictStrategy {
184 LastWriteWins,
186 HigherVersionWins,
188 Union,
190 Intersection,
192 Custom(String),
194}
195
196impl fmt::Display for ConflictStrategy {
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 match self {
199 Self::LastWriteWins => write!(f, "LastWriteWins"),
200 Self::HigherVersionWins => write!(f, "HigherVersionWins"),
201 Self::Union => write!(f, "Union"),
202 Self::Intersection => write!(f, "Intersection"),
203 Self::Custom(name) => write!(f, "Custom({})", name),
204 }
205 }
206}
207
208#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct VersionedRuleSet {
215 pub version: RuleSetVersion,
216 pub rules: Vec<String>,
217}
218
219impl VersionedRuleSet {
220 pub fn new(version: RuleSetVersion, rules: Vec<String>) -> Self {
222 Self { version, rules }
223 }
224
225 pub fn rule_count(&self) -> usize {
227 self.rules.len()
228 }
229}
230
231#[derive(Debug, Clone)]
237pub struct ResolvedRuleSet {
238 pub rules: Vec<String>,
240 pub strategy_used: String,
242 pub conflict_detected: bool,
245 pub source_versions: (RuleSetVersion, RuleSetVersion),
247}
248
249pub struct ConflictResolver {
256 pub strategy: ConflictStrategy,
257}
258
259impl ConflictResolver {
260 pub fn new(strategy: ConflictStrategy) -> Self {
262 Self { strategy }
263 }
264
265 pub fn resolve(&self, local: &VersionedRuleSet, remote: &VersionedRuleSet) -> ResolvedRuleSet {
268 let conflict_detected = local.version.fingerprint != remote.version.fingerprint;
269 let strategy_used = self.strategy.to_string();
270
271 let rules = match &self.strategy {
272 ConflictStrategy::LastWriteWins => {
273 if remote.version.created_at > local.version.created_at {
274 remote.rules.clone()
275 } else {
276 local.rules.clone()
277 }
278 }
279
280 ConflictStrategy::HigherVersionWins => {
281 let local_ver = (
282 local.version.major,
283 local.version.minor,
284 local.version.patch,
285 );
286 let remote_ver = (
287 remote.version.major,
288 remote.version.minor,
289 remote.version.patch,
290 );
291 if remote_ver > local_ver {
292 remote.rules.clone()
293 } else {
294 local.rules.clone()
295 }
296 }
297
298 ConflictStrategy::Union | ConflictStrategy::Custom(_) => {
299 let mut merged: Vec<String> = local
301 .rules
302 .iter()
303 .chain(remote.rules.iter())
304 .cloned()
305 .collect::<HashSet<String>>()
306 .into_iter()
307 .collect();
308 merged.sort_unstable();
309 merged
310 }
311
312 ConflictStrategy::Intersection => {
313 let local_set: HashSet<&str> = local.rules.iter().map(String::as_str).collect();
314 let remote_set: HashSet<&str> = remote.rules.iter().map(String::as_str).collect();
315 let mut intersection: Vec<String> = local_set
316 .intersection(&remote_set)
317 .map(|s| (*s).to_string())
318 .collect();
319 intersection.sort_unstable();
320 intersection
321 }
322 };
323
324 ResolvedRuleSet {
325 rules,
326 strategy_used,
327 conflict_detected,
328 source_versions: (local.version.clone(), remote.version.clone()),
329 }
330 }
331}
332
333#[cfg(test)]
338mod tests {
339 use super::*;
340
341 fn make_vrs(
343 major: u32,
344 minor: u32,
345 patch: u32,
346 rules: &[&str],
347 author: &str,
348 created_at_override: Option<u64>,
349 ) -> VersionedRuleSet {
350 let rule_strings: Vec<String> = rules.iter().map(|s| s.to_string()).collect();
351 let mut ver = RuleSetVersion::new(major, minor, patch, &rule_strings, author);
352 if let Some(ts) = created_at_override {
353 ver.created_at = ts;
354 }
355 VersionedRuleSet::new(ver, rule_strings)
356 }
357
358 #[test]
363 fn test_version_display_format() {
364 let rules = vec!["rule_a".to_string(), "rule_b".to_string()];
365 let v = RuleSetVersion::new(1, 2, 3, &rules, "peer-1");
366 let s = v.to_string();
367 assert!(s.starts_with("1.2.3+"), "got: {s}");
369 let parts: Vec<&str> = s.splitn(2, '+').collect();
371 assert_eq!(parts.len(), 2);
372 assert_eq!(
373 parts[1].len(),
374 16,
375 "fingerprint hex should be 16 chars: {s}"
376 );
377 assert!(
378 parts[1].chars().all(|c| c.is_ascii_hexdigit()),
379 "fingerprint not hex: {s}"
380 );
381 }
382
383 #[test]
384 fn test_version_display_zero_patch() {
385 let rules: Vec<String> = vec![];
386 let v = RuleSetVersion::new(0, 0, 0, &rules, "peer");
387 assert!(v.to_string().starts_with("0.0.0+"));
388 }
389
390 #[test]
395 fn test_compatible_same_major() {
396 let rules: Vec<String> = vec!["r1".to_string()];
397 let v1 = RuleSetVersion::new(2, 0, 0, &rules, "a");
398 let v2 = RuleSetVersion::new(2, 5, 3, &rules, "b");
399 assert!(v1.is_compatible_with(&v2));
400 assert!(v2.is_compatible_with(&v1));
401 }
402
403 #[test]
404 fn test_incompatible_different_major() {
405 let rules: Vec<String> = vec!["r1".to_string()];
406 let v1 = RuleSetVersion::new(1, 9, 9, &rules, "a");
407 let v2 = RuleSetVersion::new(2, 0, 0, &rules, "b");
408 assert!(!v1.is_compatible_with(&v2));
409 assert!(!v2.is_compatible_with(&v1));
410 }
411
412 #[test]
413 fn test_compatible_same_version() {
414 let rules: Vec<String> = vec![];
415 let v = RuleSetVersion::new(3, 1, 4, &rules, "peer");
416 assert!(v.is_compatible_with(&v));
417 }
418
419 #[test]
424 fn test_fingerprint_order_independent() {
425 let rules_ab = vec!["rule_a".to_string(), "rule_b".to_string()];
426 let rules_ba = vec!["rule_b".to_string(), "rule_a".to_string()];
427 let v1 = RuleSetVersion::new(1, 0, 0, &rules_ab, "peer");
428 let v2 = RuleSetVersion::new(1, 0, 0, &rules_ba, "peer");
429 assert_eq!(v1.fingerprint, v2.fingerprint);
430 }
431
432 #[test]
437 fn test_diff_added_removed_unchanged() {
438 let old = vec!["a".to_string(), "b".to_string(), "c".to_string()];
439 let new = vec!["b".to_string(), "c".to_string(), "d".to_string()];
440 let diff = RuleSetDiff::diff(&old, &new);
441 assert_eq!(diff.added, vec!["d".to_string()]);
442 assert_eq!(diff.removed, vec!["a".to_string()]);
443 assert_eq!(diff.unchanged, 2);
444 }
445
446 #[test]
447 fn test_diff_no_change() {
448 let rules = vec!["x".to_string(), "y".to_string()];
449 let diff = RuleSetDiff::diff(&rules, &rules);
450 assert!(diff.is_empty());
451 assert_eq!(diff.unchanged, 2);
452 }
453
454 #[test]
455 fn test_diff_all_added() {
456 let old: Vec<String> = vec![];
457 let new = vec!["r1".to_string(), "r2".to_string()];
458 let diff = RuleSetDiff::diff(&old, &new);
459 assert_eq!(diff.added.len(), 2);
460 assert!(diff.removed.is_empty());
461 assert_eq!(diff.unchanged, 0);
462 }
463
464 #[test]
465 fn test_diff_all_removed() {
466 let old = vec!["r1".to_string(), "r2".to_string()];
467 let new: Vec<String> = vec![];
468 let diff = RuleSetDiff::diff(&old, &new);
469 assert!(diff.added.is_empty());
470 assert_eq!(diff.removed.len(), 2);
471 assert_eq!(diff.unchanged, 0);
472 }
473
474 #[test]
475 fn test_diff_is_empty_false() {
476 let old = vec!["a".to_string()];
477 let new = vec!["b".to_string()];
478 let diff = RuleSetDiff::diff(&old, &new);
479 assert!(!diff.is_empty());
480 }
481
482 #[test]
483 fn test_diff_summary_format() {
484 let old = vec!["a".to_string(), "b".to_string(), "c".to_string()];
485 let new = vec![
486 "b".to_string(),
487 "c".to_string(),
488 "d".to_string(),
489 "e".to_string(),
490 ];
491 let diff = RuleSetDiff::diff(&old, &new);
492 assert_eq!(diff.summary(), "+2/-1/=2");
494 }
495
496 #[test]
497 fn test_diff_summary_no_change() {
498 let rules = vec!["x".to_string()];
499 let diff = RuleSetDiff::diff(&rules, &rules);
500 assert_eq!(diff.summary(), "+0/-0/=1");
501 }
502
503 #[test]
508 fn test_last_write_wins_picks_higher_timestamp() {
509 let local = make_vrs(1, 0, 0, &["rule_local"], "local", Some(100));
510 let remote = make_vrs(1, 0, 0, &["rule_remote"], "remote", Some(200));
511 let resolver = ConflictResolver::new(ConflictStrategy::LastWriteWins);
512 let result = resolver.resolve(&local, &remote);
513 assert_eq!(result.rules, vec!["rule_remote".to_string()]);
514 assert_eq!(result.strategy_used, "LastWriteWins");
515 }
516
517 #[test]
518 fn test_last_write_wins_picks_local_when_newer() {
519 let local = make_vrs(1, 0, 0, &["rule_local"], "local", Some(999));
520 let remote = make_vrs(1, 0, 0, &["rule_remote"], "remote", Some(1));
521 let resolver = ConflictResolver::new(ConflictStrategy::LastWriteWins);
522 let result = resolver.resolve(&local, &remote);
523 assert_eq!(result.rules, vec!["rule_local".to_string()]);
524 }
525
526 #[test]
531 fn test_higher_version_wins_remote_major() {
532 let local = make_vrs(1, 9, 9, &["local_rule"], "local", None);
533 let remote = make_vrs(2, 0, 0, &["remote_rule"], "remote", None);
534 let resolver = ConflictResolver::new(ConflictStrategy::HigherVersionWins);
535 let result = resolver.resolve(&local, &remote);
536 assert_eq!(result.rules, vec!["remote_rule".to_string()]);
537 }
538
539 #[test]
540 fn test_higher_version_wins_local_minor() {
541 let local = make_vrs(1, 5, 0, &["local_rule"], "local", None);
542 let remote = make_vrs(1, 3, 0, &["remote_rule"], "remote", None);
543 let resolver = ConflictResolver::new(ConflictStrategy::HigherVersionWins);
544 let result = resolver.resolve(&local, &remote);
545 assert_eq!(result.rules, vec!["local_rule".to_string()]);
546 }
547
548 #[test]
549 fn test_higher_version_wins_patch_tiebreak() {
550 let local = make_vrs(2, 1, 3, &["local_rule"], "local", None);
551 let remote = make_vrs(2, 1, 7, &["remote_rule"], "remote", None);
552 let resolver = ConflictResolver::new(ConflictStrategy::HigherVersionWins);
553 let result = resolver.resolve(&local, &remote);
554 assert_eq!(result.rules, vec!["remote_rule".to_string()]);
555 }
556
557 #[test]
562 fn test_union_merges_and_deduplicates() {
563 let local = make_vrs(1, 0, 0, &["a", "b", "c"], "local", None);
564 let remote = make_vrs(1, 0, 0, &["b", "c", "d"], "remote", None);
565 let resolver = ConflictResolver::new(ConflictStrategy::Union);
566 let result = resolver.resolve(&local, &remote);
567 let expected = vec!["a", "b", "c", "d"]
568 .into_iter()
569 .map(String::from)
570 .collect::<Vec<_>>();
571 assert_eq!(result.rules, expected);
572 }
573
574 #[test]
575 fn test_union_identical_sets() {
576 let local = make_vrs(1, 0, 0, &["x", "y"], "local", None);
577 let remote = make_vrs(1, 0, 0, &["x", "y"], "remote", None);
578 let resolver = ConflictResolver::new(ConflictStrategy::Union);
579 let result = resolver.resolve(&local, &remote);
580 assert_eq!(result.rules, vec!["x".to_string(), "y".to_string()]);
581 }
582
583 #[test]
588 fn test_intersection_returns_common_rules() {
589 let local = make_vrs(1, 0, 0, &["a", "b", "c"], "local", None);
590 let remote = make_vrs(1, 0, 0, &["b", "c", "d"], "remote", None);
591 let resolver = ConflictResolver::new(ConflictStrategy::Intersection);
592 let result = resolver.resolve(&local, &remote);
593 let expected = vec!["b".to_string(), "c".to_string()];
594 assert_eq!(result.rules, expected);
595 }
596
597 #[test]
598 fn test_intersection_empty_when_disjoint() {
599 let local = make_vrs(1, 0, 0, &["a", "b"], "local", None);
600 let remote = make_vrs(1, 0, 0, &["c", "d"], "remote", None);
601 let resolver = ConflictResolver::new(ConflictStrategy::Intersection);
602 let result = resolver.resolve(&local, &remote);
603 assert!(result.rules.is_empty());
604 }
605
606 #[test]
611 fn test_custom_strategy_union_fallback() {
612 let local = make_vrs(1, 0, 0, &["p", "q"], "local", None);
613 let remote = make_vrs(1, 0, 0, &["q", "r"], "remote", None);
614 let resolver = ConflictResolver::new(ConflictStrategy::Custom("my-policy".to_string()));
615 let result = resolver.resolve(&local, &remote);
616 let expected = vec!["p".to_string(), "q".to_string(), "r".to_string()];
617 assert_eq!(result.rules, expected);
618 assert_eq!(result.strategy_used, "Custom(my-policy)");
619 }
620
621 #[test]
626 fn test_conflict_detected_different_fingerprints() {
627 let local = make_vrs(1, 0, 0, &["rule_a"], "local", None);
628 let remote = make_vrs(1, 0, 0, &["rule_b"], "remote", None);
629 assert_ne!(local.version.fingerprint, remote.version.fingerprint);
631 let resolver = ConflictResolver::new(ConflictStrategy::Union);
632 let result = resolver.resolve(&local, &remote);
633 assert!(result.conflict_detected);
634 }
635
636 #[test]
637 fn test_conflict_not_detected_identical_fingerprints() {
638 let rules = vec!["rule_a".to_string(), "rule_b".to_string()];
639 let ver_local = RuleSetVersion::new(1, 0, 0, &rules, "local");
641 let ver_remote = RuleSetVersion::new(1, 0, 1, &rules, "remote");
642 assert_eq!(ver_local.fingerprint, ver_remote.fingerprint);
644 let local = VersionedRuleSet::new(ver_local, rules.clone());
645 let remote = VersionedRuleSet::new(ver_remote, rules);
646 let resolver = ConflictResolver::new(ConflictStrategy::Union);
647 let result = resolver.resolve(&local, &remote);
648 assert!(!result.conflict_detected);
649 }
650
651 #[test]
656 fn test_versioned_rule_set_rule_count() {
657 let vrs = make_vrs(1, 0, 0, &["a", "b", "c", "d", "e"], "peer", None);
658 assert_eq!(vrs.rule_count(), 5);
659 }
660
661 #[test]
666 fn test_source_versions_preserved() {
667 let local = make_vrs(1, 0, 0, &["l"], "local-peer", None);
668 let remote = make_vrs(2, 0, 0, &["r"], "remote-peer", None);
669 let resolver = ConflictResolver::new(ConflictStrategy::HigherVersionWins);
670 let result = resolver.resolve(&local, &remote);
671 assert_eq!(result.source_versions.0.author_peer_id, "local-peer");
672 assert_eq!(result.source_versions.1.author_peer_id, "remote-peer");
673 }
674}