Skip to main content

oxihuman_core/
symlink_resolver.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Symbolic link resolver stub.
6
7use std::collections::HashMap;
8
9/// A symbolic link record.
10#[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
25/// Symlink resolver — holds a virtual symlink table.
26pub 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(&current) {
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
69/// Create a new resolver with default settings.
70pub fn new_symlink_resolver() -> SymlinkResolver {
71    SymlinkResolver::default()
72}
73
74/// Register multiple symlinks at once.
75pub 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
81/// Resolve a batch of paths.
82pub fn resolve_batch(resolver: &SymlinkResolver, paths: &[&str]) -> Vec<Result<String, String>> {
83    paths.iter().map(|p| resolver.resolve(p)).collect()
84}
85
86/// Check for cycles: returns the paths involved if a loop is detected.
87pub 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}