1use std::path::{Path, PathBuf};
23
24use crate::fs::Fs;
25
26pub fn resolve_symlink_target(link_path: &Path, raw_target: &Path) -> PathBuf {
38 if raw_target.is_absolute() {
39 raw_target.to_path_buf()
40 } else {
41 link_path
42 .parent()
43 .unwrap_or_else(|| Path::new(""))
44 .join(raw_target)
45 }
46}
47
48pub fn normalize_path(path: &Path) -> PathBuf {
59 use std::path::Component;
60 let mut result = PathBuf::new();
61 for component in path.components() {
62 match component {
63 Component::CurDir => {}
64 Component::ParentDir => {
65 result.pop();
66 }
67 other => result.push(other),
68 }
69 }
70 result
71}
72
73pub fn is_equivalent(user_path: &Path, source: &Path, fs: &dyn Fs) -> bool {
79 if fs.is_symlink(user_path) {
80 match fs.readlink(user_path) {
85 Ok(target) => resolve_symlink_target(user_path, &target) == source,
86 Err(_) => false,
87 }
88 } else if fs.exists(user_path) && !fs.is_dir(user_path) {
89 match (fs.read_file(user_path), fs.read_file(source)) {
91 (Ok(a), Ok(b)) => a == b,
92 _ => false,
93 }
94 } else {
95 false
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::testing::TempEnvironment;
104
105 #[test]
106 fn relative_direct_symlink_to_source_is_equivalent() {
107 let env = TempEnvironment::builder()
115 .pack("vim")
116 .file("vimrc", "set nocompatible")
117 .done()
118 .build();
119
120 let source = env.dotfiles_root.join("vim/vimrc");
121 let user_path = env.home.join(".vimrc");
122 let relative_target = std::path::PathBuf::from("dotfiles/vim/vimrc");
123 env.fs.symlink(&relative_target, &user_path).unwrap();
124
125 assert_eq!(
129 resolve_symlink_target(&user_path, &relative_target),
130 source,
131 "test layout assumption broke: relative target doesn't resolve to source"
132 );
133
134 assert!(
135 is_equivalent(&user_path, &source, env.fs.as_ref()),
136 "relative symlink to source should be equivalent (resolved against link's parent)"
137 );
138 }
139
140 #[test]
141 fn direct_symlink_to_source_is_equivalent() {
142 let env = TempEnvironment::builder()
143 .pack("vim")
144 .file("vimrc", "set nocompatible")
145 .done()
146 .build();
147
148 let source = env.dotfiles_root.join("vim/vimrc");
149 let user_path = env.home.join(".vimrc");
150 env.fs.symlink(&source, &user_path).unwrap();
151
152 assert!(is_equivalent(&user_path, &source, env.fs.as_ref()));
153 }
154
155 #[test]
156 fn symlink_pointing_elsewhere_is_not_equivalent() {
157 let env = TempEnvironment::builder()
158 .pack("vim")
159 .file("vimrc", "set nocompatible")
160 .done()
161 .build();
162
163 let source = env.dotfiles_root.join("vim/vimrc");
164 let user_path = env.home.join(".vimrc");
165 env.fs
166 .symlink(std::path::Path::new("/tmp/somewhere-else"), &user_path)
167 .unwrap();
168
169 assert!(!is_equivalent(&user_path, &source, env.fs.as_ref()));
170 }
171
172 #[test]
173 fn multi_hop_symlink_to_source_is_not_equivalent() {
174 let env = TempEnvironment::builder()
177 .pack("vim")
178 .file("vimrc", "set nocompatible")
179 .done()
180 .build();
181
182 let source = env.dotfiles_root.join("vim/vimrc");
183 let intermediate = env.home.join(".vimrc.intermediate");
184 let user_path = env.home.join(".vimrc");
185 env.fs.symlink(&source, &intermediate).unwrap();
186 env.fs.symlink(&intermediate, &user_path).unwrap();
187
188 assert!(!is_equivalent(&user_path, &source, env.fs.as_ref()));
189 }
190
191 #[test]
192 fn regular_file_with_identical_content_is_equivalent() {
193 let env = TempEnvironment::builder()
194 .pack("git")
195 .file("gitconfig", "[user]\n name = test")
196 .done()
197 .home_file(".gitconfig", "[user]\n name = test")
198 .build();
199
200 let source = env.dotfiles_root.join("git/gitconfig");
201 let user_path = env.home.join(".gitconfig");
202
203 assert!(is_equivalent(&user_path, &source, env.fs.as_ref()));
204 }
205
206 #[test]
207 fn regular_file_with_different_content_is_not_equivalent() {
208 let env = TempEnvironment::builder()
209 .pack("git")
210 .file("gitconfig", "[user]\n name = new")
211 .done()
212 .home_file(".gitconfig", "[user]\n name = old")
213 .build();
214
215 let source = env.dotfiles_root.join("git/gitconfig");
216 let user_path = env.home.join(".gitconfig");
217
218 assert!(!is_equivalent(&user_path, &source, env.fs.as_ref()));
219 }
220
221 #[test]
222 fn absent_user_path_is_not_equivalent() {
223 let env = TempEnvironment::builder()
224 .pack("vim")
225 .file("vimrc", "x")
226 .done()
227 .build();
228
229 let source = env.dotfiles_root.join("vim/vimrc");
230 let user_path = env.home.join(".vimrc");
231 assert!(!is_equivalent(&user_path, &source, env.fs.as_ref()));
234 }
235
236 #[test]
237 fn directory_is_not_equivalent() {
238 let env = TempEnvironment::builder()
239 .pack("vim")
240 .file("vimrc", "x")
241 .done()
242 .build();
243
244 let source = env.dotfiles_root.join("vim/vimrc");
245 let user_path = env.home.join(".vimrc");
246 env.fs.mkdir_all(&user_path).unwrap();
247
248 assert!(!is_equivalent(&user_path, &source, env.fs.as_ref()));
249 }
250
251 #[test]
252 fn broken_symlink_is_not_equivalent() {
253 let env = TempEnvironment::builder()
254 .pack("vim")
255 .file("vimrc", "x")
256 .done()
257 .build();
258
259 let source = env.dotfiles_root.join("vim/vimrc");
260 let user_path = env.home.join(".vimrc");
261 env.fs
262 .symlink(std::path::Path::new("/does/not/exist"), &user_path)
263 .unwrap();
264
265 assert!(!is_equivalent(&user_path, &source, env.fs.as_ref()));
266 }
267}