Skip to main content

reddb_server/storage/engine/algorithms/
cycles.rs

1//! Cycle Detection Algorithm
2//!
3//! DFS-based cycle detection for finding:
4//! - Lateral movement loops
5//! - Circular dependencies
6//! - Trust cycles
7
8use std::collections::HashSet;
9
10use super::super::graph_store::GraphStore;
11
12// ============================================================================
13// Cycle Detection
14// ============================================================================
15
16/// DFS-based cycle detection
17///
18/// Finds cycles in the graph, useful for detecting:
19/// - Lateral movement loops
20/// - Circular dependencies
21/// - Trust cycles
22pub struct CycleDetector {
23    /// Maximum cycle length to find
24    pub max_length: usize,
25    /// Maximum number of cycles to return
26    pub max_cycles: usize,
27}
28
29impl Default for CycleDetector {
30    fn default() -> Self {
31        Self {
32            max_length: 10,
33            max_cycles: 100,
34        }
35    }
36}
37
38/// A cycle in the graph
39#[derive(Debug, Clone)]
40pub struct Cycle {
41    /// Nodes in the cycle (first == last)
42    pub nodes: Vec<String>,
43    /// Length of the cycle (number of edges)
44    pub length: usize,
45}
46
47/// Result of cycle detection
48#[derive(Debug, Clone)]
49pub struct CyclesResult {
50    /// Found cycles
51    pub cycles: Vec<Cycle>,
52    /// Whether max_cycles limit was reached
53    pub limit_reached: bool,
54}
55
56impl CycleDetector {
57    /// Create with default parameters
58    pub fn new() -> Self {
59        Self::default()
60    }
61
62    /// Set maximum cycle length
63    pub fn max_length(mut self, max: usize) -> Self {
64        self.max_length = max;
65        self
66    }
67
68    /// Set maximum number of cycles to find
69    pub fn max_cycles(mut self, max: usize) -> Self {
70        self.max_cycles = max;
71        self
72    }
73
74    /// Find all cycles in the graph
75    pub fn find(&self, graph: &GraphStore) -> CyclesResult {
76        let nodes: Vec<String> = graph.iter_nodes().map(|n| n.id.clone()).collect();
77        let mut cycles: Vec<Cycle> = Vec::new();
78        let mut visited_global: HashSet<String> = HashSet::new();
79
80        for start in &nodes {
81            if cycles.len() >= self.max_cycles {
82                return CyclesResult {
83                    cycles,
84                    limit_reached: true,
85                };
86            }
87
88            // DFS from this node
89            let mut stack: Vec<(String, Vec<String>)> = vec![(start.clone(), vec![start.clone()])];
90            let mut visited_in_path: HashSet<String> = HashSet::new();
91            visited_in_path.insert(start.clone());
92
93            while let Some((current, path)) = stack.pop() {
94                // Skip if path already exceeds max_length (length = edges = nodes - 1)
95                if path.len() > self.max_length {
96                    continue;
97                }
98
99                for (_, neighbor, _) in graph.outgoing_edges(&current) {
100                    // Found a cycle back to start
101                    if neighbor == *start && path.len() > 1 {
102                        let mut cycle_nodes = path.clone();
103                        cycle_nodes.push(start.clone());
104
105                        // Check if this is a new cycle (not just a rotation)
106                        if !Self::is_duplicate_cycle(&cycles, &cycle_nodes) {
107                            cycles.push(Cycle {
108                                length: cycle_nodes.len() - 1,
109                                nodes: cycle_nodes,
110                            });
111
112                            if cycles.len() >= self.max_cycles {
113                                return CyclesResult {
114                                    cycles,
115                                    limit_reached: true,
116                                };
117                            }
118                        }
119                    } else if !visited_in_path.contains(&neighbor)
120                        && !visited_global.contains(&neighbor)
121                    {
122                        let mut new_path = path.clone();
123                        new_path.push(neighbor.clone());
124                        visited_in_path.insert(neighbor.clone());
125                        stack.push((neighbor, new_path));
126                    }
127                }
128            }
129
130            visited_global.insert(start.clone());
131        }
132
133        CyclesResult {
134            cycles,
135            limit_reached: false,
136        }
137    }
138
139    /// Check if a cycle is a rotation of an existing one
140    fn is_duplicate_cycle(existing: &[Cycle], new_cycle: &[String]) -> bool {
141        if new_cycle.len() < 2 {
142            return true;
143        }
144
145        // Get cycle without the repeated end node
146        let cycle_core: Vec<&str> = new_cycle[..new_cycle.len() - 1]
147            .iter()
148            .map(|s| s.as_str())
149            .collect();
150
151        for existing_cycle in existing {
152            if existing_cycle.length != cycle_core.len() {
153                continue;
154            }
155
156            let existing_core: Vec<&str> = existing_cycle.nodes[..existing_cycle.nodes.len() - 1]
157                .iter()
158                .map(|s| s.as_str())
159                .collect();
160
161            // Check if it's a rotation
162            if Self::is_rotation(&existing_core, &cycle_core) {
163                return true;
164            }
165        }
166
167        false
168    }
169
170    /// Check if two sequences are rotations of each other
171    fn is_rotation(a: &[&str], b: &[&str]) -> bool {
172        if a.len() != b.len() {
173            return false;
174        }
175
176        // Try each rotation
177        let n = a.len();
178        for i in 0..n {
179            let mut matches = true;
180            for j in 0..n {
181                if a[j] != b[(i + j) % n] {
182                    matches = false;
183                    break;
184                }
185            }
186            if matches {
187                return true;
188            }
189        }
190
191        false
192    }
193}