oxihuman_core/
symlink_resolver.rs1#![allow(dead_code)]
4
5use std::collections::HashMap;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Symlink {
12 pub path: String,
13 pub target: String,
14}
15
16impl Symlink {
17 pub fn new(path: &str, target: &str) -> Self {
18 Symlink {
19 path: path.to_string(),
20 target: target.to_string(),
21 }
22 }
23}
24
25pub struct SymlinkResolver {
27 table: HashMap<String, String>,
28 max_depth: usize,
29}
30
31impl SymlinkResolver {
32 pub fn new(max_depth: usize) -> Self {
33 SymlinkResolver {
34 table: HashMap::new(),
35 max_depth,
36 }
37 }
38
39 pub fn register(&mut self, link: Symlink) {
40 self.table.insert(link.path, link.target);
41 }
42
43 pub fn resolve(&self, path: &str) -> Result<String, String> {
44 let mut current = path.to_string();
45 for _ in 0..self.max_depth {
46 match self.table.get(¤t) {
47 Some(target) => current = target.clone(),
48 None => return Ok(current),
49 }
50 }
51 Err(format!("symlink loop detected at '{}'", path))
52 }
53
54 pub fn is_symlink(&self, path: &str) -> bool {
55 self.table.contains_key(path)
56 }
57
58 pub fn count(&self) -> usize {
59 self.table.len()
60 }
61}
62
63impl Default for SymlinkResolver {
64 fn default() -> Self {
65 Self::new(40)
66 }
67}
68
69pub fn new_symlink_resolver() -> SymlinkResolver {
71 SymlinkResolver::default()
72}
73
74pub fn register_all(resolver: &mut SymlinkResolver, links: &[(&str, &str)]) {
76 for (path, target) in links {
77 resolver.register(Symlink::new(path, target));
78 }
79}
80
81pub fn resolve_batch(resolver: &SymlinkResolver, paths: &[&str]) -> Vec<Result<String, String>> {
83 paths.iter().map(|p| resolver.resolve(p)).collect()
84}
85
86pub fn detect_cycle(resolver: &SymlinkResolver, start: &str) -> bool {
88 resolver.resolve(start).is_err()
89}
90
91#[cfg(test)]
92mod tests {
93 use super::*;
94
95 #[test]
96 fn test_resolve_non_symlink() {
97 let r = new_symlink_resolver();
98 assert_eq!(r.resolve("/real"), Ok("/real".to_string()));
99 }
100
101 #[test]
102 fn test_resolve_single_hop() {
103 let mut r = new_symlink_resolver();
104 r.register(Symlink::new("/link", "/target"));
105 assert_eq!(r.resolve("/link"), Ok("/target".to_string()));
106 }
107
108 #[test]
109 fn test_resolve_chain() {
110 let mut r = new_symlink_resolver();
111 r.register(Symlink::new("/a", "/b"));
112 r.register(Symlink::new("/b", "/c"));
113 assert_eq!(r.resolve("/a"), Ok("/c".to_string()));
114 }
115
116 #[test]
117 fn test_detect_cycle() {
118 let mut r = SymlinkResolver::new(4);
119 r.register(Symlink::new("/x", "/y"));
120 r.register(Symlink::new("/y", "/x"));
121 assert!(detect_cycle(&r, "/x"));
122 }
123
124 #[test]
125 fn test_is_symlink() {
126 let mut r = new_symlink_resolver();
127 r.register(Symlink::new("/sym", "/real"));
128 assert!(r.is_symlink("/sym"));
129 assert!(!r.is_symlink("/real"));
130 }
131
132 #[test]
133 fn test_count() {
134 let mut r = new_symlink_resolver();
135 register_all(&mut r, &[("/a", "/b"), ("/c", "/d")]);
136 assert_eq!(r.count(), 2);
137 }
138
139 #[test]
140 fn test_resolve_batch() {
141 let mut r = new_symlink_resolver();
142 r.register(Symlink::new("/link", "/real"));
143 let results = resolve_batch(&r, &["/link", "/other"]);
144 assert_eq!(results[0], Ok("/real".to_string()));
145 assert_eq!(results[1], Ok("/other".to_string()));
146 }
147
148 #[test]
149 fn test_register_all() {
150 let mut r = new_symlink_resolver();
151 register_all(&mut r, &[("/p", "/q")]);
152 assert!(r.is_symlink("/p"));
153 }
154
155 #[test]
156 fn test_default_max_depth() {
157 let r = SymlinkResolver::default();
158 assert_eq!(r.max_depth, 40);
159 }
160
161 #[test]
162 fn test_resolve_ok_not_err_on_real_path() {
163 let r = new_symlink_resolver();
164 assert!(r.resolve("/real/path").is_ok());
165 }
166}