use std::path::{Path, PathBuf};
use crate::fs::Fs;
pub fn resolve_symlink_target(link_path: &Path, raw_target: &Path) -> PathBuf {
if raw_target.is_absolute() {
raw_target.to_path_buf()
} else {
link_path
.parent()
.unwrap_or_else(|| Path::new(""))
.join(raw_target)
}
}
pub fn normalize_path(path: &Path) -> PathBuf {
use std::path::Component;
let mut result = PathBuf::new();
for component in path.components() {
match component {
Component::CurDir => {}
Component::ParentDir => {
result.pop();
}
other => result.push(other),
}
}
result
}
pub fn is_equivalent(user_path: &Path, source: &Path, fs: &dyn Fs) -> bool {
if fs.is_symlink(user_path) {
match fs.readlink(user_path) {
Ok(target) => resolve_symlink_target(user_path, &target) == source,
Err(_) => false,
}
} else if fs.exists(user_path) && !fs.is_dir(user_path) {
match (fs.read_file(user_path), fs.read_file(source)) {
(Ok(a), Ok(b)) => a == b,
_ => false,
}
} else {
false
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::TempEnvironment;
#[test]
fn relative_direct_symlink_to_source_is_equivalent() {
let env = TempEnvironment::builder()
.pack("vim")
.file("vimrc", "set nocompatible")
.done()
.build();
let source = env.dotfiles_root.join("vim/vimrc");
let user_path = env.home.join(".vimrc");
let relative_target = std::path::PathBuf::from("dotfiles/vim/vimrc");
env.fs.symlink(&relative_target, &user_path).unwrap();
assert_eq!(
resolve_symlink_target(&user_path, &relative_target),
source,
"test layout assumption broke: relative target doesn't resolve to source"
);
assert!(
is_equivalent(&user_path, &source, env.fs.as_ref()),
"relative symlink to source should be equivalent (resolved against link's parent)"
);
}
#[test]
fn direct_symlink_to_source_is_equivalent() {
let env = TempEnvironment::builder()
.pack("vim")
.file("vimrc", "set nocompatible")
.done()
.build();
let source = env.dotfiles_root.join("vim/vimrc");
let user_path = env.home.join(".vimrc");
env.fs.symlink(&source, &user_path).unwrap();
assert!(is_equivalent(&user_path, &source, env.fs.as_ref()));
}
#[test]
fn symlink_pointing_elsewhere_is_not_equivalent() {
let env = TempEnvironment::builder()
.pack("vim")
.file("vimrc", "set nocompatible")
.done()
.build();
let source = env.dotfiles_root.join("vim/vimrc");
let user_path = env.home.join(".vimrc");
env.fs
.symlink(std::path::Path::new("/tmp/somewhere-else"), &user_path)
.unwrap();
assert!(!is_equivalent(&user_path, &source, env.fs.as_ref()));
}
#[test]
fn multi_hop_symlink_to_source_is_not_equivalent() {
let env = TempEnvironment::builder()
.pack("vim")
.file("vimrc", "set nocompatible")
.done()
.build();
let source = env.dotfiles_root.join("vim/vimrc");
let intermediate = env.home.join(".vimrc.intermediate");
let user_path = env.home.join(".vimrc");
env.fs.symlink(&source, &intermediate).unwrap();
env.fs.symlink(&intermediate, &user_path).unwrap();
assert!(!is_equivalent(&user_path, &source, env.fs.as_ref()));
}
#[test]
fn regular_file_with_identical_content_is_equivalent() {
let env = TempEnvironment::builder()
.pack("git")
.file("gitconfig", "[user]\n name = test")
.done()
.home_file(".gitconfig", "[user]\n name = test")
.build();
let source = env.dotfiles_root.join("git/gitconfig");
let user_path = env.home.join(".gitconfig");
assert!(is_equivalent(&user_path, &source, env.fs.as_ref()));
}
#[test]
fn regular_file_with_different_content_is_not_equivalent() {
let env = TempEnvironment::builder()
.pack("git")
.file("gitconfig", "[user]\n name = new")
.done()
.home_file(".gitconfig", "[user]\n name = old")
.build();
let source = env.dotfiles_root.join("git/gitconfig");
let user_path = env.home.join(".gitconfig");
assert!(!is_equivalent(&user_path, &source, env.fs.as_ref()));
}
#[test]
fn absent_user_path_is_not_equivalent() {
let env = TempEnvironment::builder()
.pack("vim")
.file("vimrc", "x")
.done()
.build();
let source = env.dotfiles_root.join("vim/vimrc");
let user_path = env.home.join(".vimrc");
assert!(!is_equivalent(&user_path, &source, env.fs.as_ref()));
}
#[test]
fn directory_is_not_equivalent() {
let env = TempEnvironment::builder()
.pack("vim")
.file("vimrc", "x")
.done()
.build();
let source = env.dotfiles_root.join("vim/vimrc");
let user_path = env.home.join(".vimrc");
env.fs.mkdir_all(&user_path).unwrap();
assert!(!is_equivalent(&user_path, &source, env.fs.as_ref()));
}
#[test]
fn broken_symlink_is_not_equivalent() {
let env = TempEnvironment::builder()
.pack("vim")
.file("vimrc", "x")
.done()
.build();
let source = env.dotfiles_root.join("vim/vimrc");
let user_path = env.home.join(".vimrc");
env.fs
.symlink(std::path::Path::new("/does/not/exist"), &user_path)
.unwrap();
assert!(!is_equivalent(&user_path, &source, env.fs.as_ref()));
}
}