use std::{fs, io, os, path::Path};
#[inline]
pub fn symlink<P: AsRef<Path>, Q: AsRef<Path>>(original: P, link: Q) -> io::Result<()> {
#[cfg(unix)]
fn inner(original: &Path, link: &Path) -> io::Result<()> {
os::unix::fs::symlink(original, link)
}
#[cfg(windows)]
fn inner(original: &Path, link: &Path) -> io::Result<()> {
let original = normalize_windows_separators(original);
let link = normalize_windows_separators(link);
let is_dir = if original.is_relative() {
link.parent()
.map(|p| p.join(original.as_ref()))
.unwrap_or_else(|| original.as_ref().to_path_buf())
.is_dir()
} else {
original.is_dir()
};
if is_dir {
os::windows::fs::symlink_dir(original.as_ref(), link.as_ref())
} else {
os::windows::fs::symlink_file(original.as_ref(), link.as_ref())
}
}
#[cfg(target_os = "wasi")]
fn inner(original: &Path, link: &Path) -> io::Result<()> {
os::wasi::fs::symlink_path(original, link)
}
inner(original.as_ref(), link.as_ref())
}
#[cfg(windows)]
fn normalize_windows_separators(path: &Path) -> std::borrow::Cow<'_, Path> {
use std::borrow::Cow;
use std::ffi::OsString;
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::path::PathBuf;
let wide: Vec<u16> = path.as_os_str().encode_wide().collect();
if !wide.iter().any(|&unit| unit == u16::from(b'/')) {
return Cow::Borrowed(path);
}
let normalized = wide
.into_iter()
.map(|unit| {
if unit == u16::from(b'/') {
u16::from(b'\\')
} else {
unit
}
})
.collect::<Vec<_>>();
Cow::Owned(PathBuf::from(OsString::from_wide(&normalized)))
}
#[inline]
fn remove_path_with<'a, F>(path: &'a Path, remove_dir_fn: F) -> io::Result<()>
where
F: FnOnce(&'a Path) -> io::Result<()>,
{
let metadata = fs::symlink_metadata(path)?;
let file_type = metadata.file_type();
if file_type.is_symlink() {
#[cfg(windows)]
{
use std::os::windows::fs::FileTypeExt;
if file_type.is_symlink_dir() {
return fs::remove_dir(path);
}
}
fs::remove_file(path)
} else if file_type.is_dir() {
remove_dir_fn(path)
} else {
fs::remove_file(path)
}
}
#[inline]
pub fn remove_path_all<P: AsRef<Path>>(path: P) -> io::Result<()> {
remove_path_with(path.as_ref(), fs::remove_dir_all)
}
#[inline]
pub fn remove_path<P: AsRef<Path>>(path: P) -> io::Result<()> {
remove_path_with(path.as_ref(), fs::remove_dir)
}
#[cfg(all(test, windows))]
mod windows_tests {
use super::normalize_windows_separators;
use std::borrow::Cow;
use std::ffi::OsString;
use std::os::windows::ffi::{OsStrExt, OsStringExt};
use std::path::{Path, PathBuf};
fn wide_units_of(path: &Path) -> Vec<u16> {
path.as_os_str().encode_wide().collect()
}
#[test]
fn returns_borrowed_when_no_forward_slash() {
let input = Path::new(r"foo\bar\baz");
let result = normalize_windows_separators(input);
assert!(matches!(result, Cow::Borrowed(_)));
assert_eq!(result.as_ref(), input);
}
#[test]
fn converts_basic_forward_slash_to_backslash() {
let result = normalize_windows_separators(Path::new("foo/bar"));
assert!(matches!(result, Cow::Owned(_)));
assert_eq!(result.as_ref(), Path::new(r"foo\bar"));
}
#[test]
fn preserves_existing_backslashes_in_mixed_input() {
let result = normalize_windows_separators(Path::new(r"a/b\c/d"));
assert_eq!(result.as_ref(), Path::new(r"a\b\c\d"));
}
#[test]
fn empty_path_returns_borrowed() {
let input = Path::new("");
let result = normalize_windows_separators(input);
assert!(matches!(result, Cow::Borrowed(_)));
assert_eq!(result.as_ref(), input);
}
#[test]
fn single_forward_slash_is_converted() {
let result = normalize_windows_separators(Path::new("/"));
assert_eq!(result.as_ref(), Path::new(r"\"));
}
#[test]
fn extended_length_path_with_forward_slashes_is_normalized() {
let result = normalize_windows_separators(Path::new(r"\\?\C:/foo/bar"));
assert_eq!(result.as_ref(), Path::new(r"\\?\C:\foo\bar"));
}
#[test]
fn lone_surrogate_is_preserved_while_slash_is_converted() {
let units: [u16; 3] = [0xD800, u16::from(b'/'), u16::from(b'a')];
let input = PathBuf::from(OsString::from_wide(&units));
let result = normalize_windows_separators(&input);
assert_eq!(
wide_units_of(result.as_ref()),
vec![0xD800, u16::from(b'\\'), u16::from(b'a')]
);
}
#[test]
fn unicode_characters_are_preserved() {
let result = normalize_windows_separators(Path::new("日本語/フォルダ"));
assert_eq!(result.as_ref(), Path::new(r"日本語\フォルダ"));
}
}