openmoat_core/
realpath.rs1use std::collections::BTreeMap;
13
14use crate::paths;
15use crate::programs::NoResolver;
16
17pub trait PathResolver {
19 fn resolve(&self, path: &str) -> Option<String>;
24
25 fn read_dir(&self, _dir: &str) -> Vec<String> {
31 Vec::new()
32 }
33}
34
35impl PathResolver for NoResolver {
36 fn resolve(&self, _path: &str) -> Option<String> {
37 None
38 }
39}
40
41#[must_use]
47pub fn physical(path: &str, resolver: &dyn PathResolver) -> String {
48 let (root, rest) = paths::split_root(path);
49 let mut here = root.clone();
50 for seg in rest.split('/') {
51 match seg {
52 "" | "." => {}
53 ".." => {
54 if let Some(real) = resolver.resolve(&here) {
55 here = paths::collapse(&real);
56 }
57 here = parent(&here, &root);
58 }
59 name => {
60 if !here.ends_with('/') {
61 here.push('/');
62 }
63 here.push_str(name);
64 }
65 }
66 }
67 here
68}
69
70fn parent(path: &str, root: &str) -> String {
72 match path.rfind('/') {
73 Some(cut) if cut >= root.len() => path[..cut].to_owned(),
74 _ => root.to_owned(),
75 }
76}
77
78#[derive(Debug, Default, Clone)]
82pub struct MapPathResolver {
83 pub links: BTreeMap<String, String>,
85 pub files: Vec<String>,
88}
89
90impl PathResolver for MapPathResolver {
91 fn read_dir(&self, dir: &str) -> Vec<String> {
93 let real = self.resolve(dir).unwrap_or_else(|| dir.to_owned());
94 let prefix = format!("{}/", real.trim_end_matches('/'));
95 let mut names: Vec<String> = self
96 .files
97 .iter()
98 .filter_map(|f| f.strip_prefix(&prefix)?.split('/').next())
99 .map(str::to_owned)
100 .collect();
101 names.sort();
102 names.dedup();
103 names
104 }
105
106 fn resolve(&self, path: &str) -> Option<String> {
107 self.links
108 .iter()
109 .filter_map(|(link, target)| {
110 let rest = path.strip_prefix(link.as_str())?;
111 (rest.is_empty() || rest.starts_with('/')).then_some((link.len(), target, rest))
112 })
113 .max_by_key(|(len, _, _)| *len)
114 .map(|(_, target, rest)| format!("{target}{rest}"))
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 #[test]
123 fn map_resolver_replaces_the_longest_linked_ancestor() {
124 let r = MapPathResolver {
125 links: BTreeMap::from([
126 ("/p/s".to_owned(), "/h/.ssh".to_owned()),
127 ("/p/s/k".to_owned(), "/h/.aws/credentials".to_owned()),
128 ]),
129 ..MapPathResolver::default()
130 };
131 assert_eq!(r.resolve("/p/s").as_deref(), Some("/h/.ssh"));
132 assert_eq!(r.resolve("/p/s/id_rsa").as_deref(), Some("/h/.ssh/id_rsa"));
133 assert_eq!(r.resolve("/p/s/k").as_deref(), Some("/h/.aws/credentials"));
134 assert_eq!(r.resolve("/p/sx/id_rsa"), None);
135 assert_eq!(r.resolve("/p/src/main.rs"), None);
136 assert_eq!(NoResolver.resolve("/p/s"), None);
137 }
138
139 #[test]
140 fn physical_steps_up_from_where_a_link_points() {
141 let r = MapPathResolver {
142 links: BTreeMap::from([("/p/c".to_owned(), "/h/.config".to_owned())]),
143 ..MapPathResolver::default()
144 };
145 assert_eq!(physical("/p/c/../.ssh/id_rsa", &r), "/h/.ssh/id_rsa");
146 assert_eq!(physical("/p/src/../README.md", &r), "/p/README.md");
147 assert_eq!(physical("/p/c/./x/../y", &r), "/h/.config/y");
148 assert_eq!(physical("/p/../../..", &r), "/");
149 assert_eq!(physical("C:/a/../b", &NoResolver), "C:/b");
150 assert_eq!(
152 physical("/p/c/../.ssh/id_rsa", &NoResolver),
153 "/p/.ssh/id_rsa"
154 );
155 }
156}