use crate::error::{Error, Result};
use crate::gen::fs;
use crate::paths;
use std::path::{Component, Path, PathBuf};
use std::{env, io};
pub(crate) fn write(path: impl AsRef<Path>, content: &[u8]) -> Result<()> {
let path = path.as_ref();
let mut create_dir_error = None;
if fs::exists(path) {
if let Ok(existing) = fs::read(path) {
if existing == content {
return Ok(());
}
}
best_effort_remove(path);
} else {
let parent = path.parent().unwrap();
create_dir_error = fs::create_dir_all(parent).err();
}
match fs::write(path, content) {
Ok(()) => Ok(()),
Err(err) => Err(Error::Fs(create_dir_error.unwrap_or(err))),
}
}
pub(crate) fn relative_symlink_file(
original: impl AsRef<Path>,
link: impl AsRef<Path>,
) -> Result<()> {
let original = original.as_ref();
let link = link.as_ref();
let parent_directory_error = prepare_parent_directory_for_symlink(link).err();
let relativized = best_effort_relativize_symlink(original, link);
symlink_file(&relativized, original, link, parent_directory_error)
}
pub(crate) fn absolute_symlink_file(
original: impl AsRef<Path>,
link: impl AsRef<Path>,
) -> Result<()> {
let original = original.as_ref();
let link = link.as_ref();
let parent_directory_error = prepare_parent_directory_for_symlink(link).err();
symlink_file(original, original, link, parent_directory_error)
}
pub(crate) fn relative_symlink_dir(
original: impl AsRef<Path>,
link: impl AsRef<Path>,
) -> Result<()> {
let original = original.as_ref();
let link = link.as_ref();
let parent_directory_error = prepare_parent_directory_for_symlink(link).err();
let relativized = best_effort_relativize_symlink(original, link);
symlink_dir(&relativized, link, parent_directory_error)
}
fn prepare_parent_directory_for_symlink(link: &Path) -> fs::Result<()> {
if fs::exists(link) {
best_effort_remove(link);
Ok(())
} else {
let parent = link.parent().unwrap();
fs::create_dir_all(parent)
}
}
fn symlink_file(
path_for_symlink: &Path,
path_for_copy: &Path,
link: &Path,
parent_directory_error: Option<fs::Error>,
) -> Result<()> {
match paths::symlink_or_copy(path_for_symlink, path_for_copy, link) {
Ok(()) => Ok(()),
Err(err) => {
if err.kind() == io::ErrorKind::AlreadyExists {
Ok(())
} else {
Err(Error::Fs(parent_directory_error.unwrap_or(err)))
}
}
}
}
fn symlink_dir(
path_for_symlink: &Path,
link: &Path,
parent_directory_error: Option<fs::Error>,
) -> Result<()> {
match fs::symlink_dir(path_for_symlink, link) {
Ok(()) => Ok(()),
Err(err) => Err(Error::Fs(parent_directory_error.unwrap_or(err))),
}
}
fn best_effort_remove(path: &Path) {
use std::fs;
if cfg!(windows) {
if let Ok(metadata) = fs::metadata(path) {
if metadata.is_dir() {
let _ = fs::remove_dir_all(path);
} else {
let _ = fs::remove_file(path);
}
} else if fs::symlink_metadata(path).is_ok() {
if fs::remove_dir_all(path).is_err() {
let _ = fs::remove_file(path);
}
}
} else {
if let Ok(metadata) = fs::symlink_metadata(path) {
if metadata.is_dir() {
let _ = fs::remove_dir_all(path);
} else {
let _ = fs::remove_file(path);
}
}
}
}
fn best_effort_relativize_symlink(original: impl AsRef<Path>, link: impl AsRef<Path>) -> PathBuf {
let original = original.as_ref();
let link = link.as_ref();
let Some(relative_path) = abstractly_relativize_symlink(original, link) else {
return original.to_path_buf();
};
if let Ok(original_canonical) = original.canonicalize() {
if let Ok(relative_canonical) = link.parent().unwrap().join(&relative_path).canonicalize() {
if original_canonical == relative_canonical {
return relative_path;
}
}
}
original.to_path_buf()
}
fn abstractly_relativize_symlink(
original: impl AsRef<Path>,
link: impl AsRef<Path>,
) -> Option<PathBuf> {
let original = original.as_ref();
let link = link.as_ref();
let likely_no_semantic_prefix = env::var_os("CARGO_TARGET_DIR").is_some();
if likely_no_semantic_prefix
|| original.is_relative()
|| link.is_relative()
|| path_contains_intermediate_components(original)
|| path_contains_intermediate_components(link)
{
return None;
}
let (common_prefix, rest_of_original, rest_of_link) = split_after_common_prefix(original, link);
if common_prefix == Path::new("") {
return None;
}
let mut rest_of_link = rest_of_link.components();
rest_of_link
.next_back()
.expect("original can't be a subdirectory of link");
let mut path_to_common_prefix = PathBuf::new();
while rest_of_link.next_back().is_some() {
path_to_common_prefix.push(Component::ParentDir);
}
Some(path_to_common_prefix.join(rest_of_original))
}
fn path_contains_intermediate_components(path: impl AsRef<Path>) -> bool {
path.as_ref()
.components()
.any(|component| component == Component::ParentDir)
}
fn split_after_common_prefix<'first, 'second>(
first: &'first Path,
second: &'second Path,
) -> (&'first Path, &'first Path, &'second Path) {
let entire_first = first;
let mut first = first.components();
let mut second = second.components();
loop {
let rest_of_first = first.as_path();
let rest_of_second = second.as_path();
match (first.next(), second.next()) {
(Some(first_component), Some(second_component))
if first_component == second_component => {}
_ => {
let mut common_prefix = entire_first;
for _ in rest_of_first.components().rev() {
if let Some(parent) = common_prefix.parent() {
common_prefix = parent;
} else {
common_prefix = Path::new("");
break;
}
}
return (common_prefix, rest_of_first, rest_of_second);
}
}
}
}
#[cfg(test)]
mod tests {
use crate::out::abstractly_relativize_symlink;
use std::path::Path;
#[cfg(not(windows))]
#[test]
fn test_relativize_symlink_unix() {
assert_eq!(
abstractly_relativize_symlink("/foo/bar/baz", "/foo/spam/eggs").as_deref(),
Some(Path::new("../bar/baz")),
);
assert_eq!(
abstractly_relativize_symlink("/foo/bar/../baz", "/foo/spam/eggs"),
None,
);
assert_eq!(
abstractly_relativize_symlink("/foo/bar/baz", "/foo/spam/./eggs").as_deref(),
Some(Path::new("../bar/baz")),
);
}
#[cfg(windows)]
#[test]
fn test_relativize_symlink_windows() {
use std::path::PathBuf;
let windows_target = PathBuf::from_iter(["c:\\", "windows", "foo"]);
let windows_link = PathBuf::from_iter(["c:\\", "users", "link"]);
let windows_different_volume_link = PathBuf::from_iter(["d:\\", "users", "link"]);
assert_eq!(
abstractly_relativize_symlink(&windows_target, windows_link).as_deref(),
Some(Path::new("..\\windows\\foo")),
);
assert_eq!(
abstractly_relativize_symlink(&windows_target, windows_different_volume_link),
None,
);
}
}