use std::borrow::Cow;
use std::collections::HashSet;
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::fs::remove_file as fs_remove_file;
use std::io;
use std::io::ErrorKind;
use std::io::Write;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
use std::path::Component;
use std::path::Path;
use std::path::PathBuf;
use anyhow::bail;
use anyhow::Context;
use fn_error_context::context;
use crate::errors::IOContext;
pub fn atomic_write_symlink(path: &Path, data: &[u8]) -> io::Result<()> {
let append_name = |suffix: &str| -> PathBuf {
let mut s = path.to_path_buf().into_os_string();
s.push(suffix);
s.into()
};
let temp_name = || -> PathBuf { append_name(&format!(".{:x}.atomic", rand::random::<u32>())) };
let _lock = crate::lock::PathLock::exclusive(append_name(".lock"))?;
let (real_path, mut file) = loop {
let real_path = temp_name();
match fs::OpenOptions::new()
.create_new(true)
.write(true)
.open(&real_path)
{
Ok(file) => break Ok((real_path, file)),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue, Err(e) => {
break Err(e).path_context("error opening atomic symlink real path", &real_path);
}
}
}?;
let real_file_name = real_path
.file_name()
.expect("real_path should have a file name");
file.write_all(data)
.path_context("error writing atomic symlink data to real path", &real_path)?;
drop(file);
let symlink_tmp_path = loop {
let symlink_path = temp_name();
match symlink_file(Path::new(real_file_name), &symlink_path) {
Ok(()) => break Ok(symlink_path),
Err(e) if e.kind() == io::ErrorKind::AlreadyExists => continue, Err(e) => {
break Err(e).path_context("error creating temp atomic symlink", &symlink_path);
}
}
}?;
fs::rename(symlink_tmp_path, path).path_context("error renaming temp atomic symlink", path)?;
let _ = (|| -> io::Result<()> {
let looks_like_atomic = |s: &OsStr, prefix: &OsStr| -> bool {
if let (Some(s), Some(prefix)) = (s.to_str(), prefix.to_str()) {
s.starts_with(prefix) && s.ends_with(".atomic")
} else {
false
}
};
if let (Some(dir), Some(prefix)) = (path.parent(), path.file_name()) {
for entry in fs::read_dir(dir).path_context("error reading atomic symlink dir", dir)? {
let entry = entry.path_context("error reading atomic symlink dir entry", dir)?;
let name = entry.file_name();
if name != prefix && looks_like_atomic(&name, prefix) && name != real_file_name {
let _ = remove_file(&entry.path());
}
}
}
Ok(())
})();
Ok(())
}
pub fn symlink_file(src: &Path, dst: &Path) -> io::Result<()> {
#[cfg(windows)]
return std::os::windows::fs::symlink_file(src, dst);
#[cfg(unix)]
return std::os::unix::fs::symlink(src, dst);
#[cfg(all(not(unix), not(windows)))]
return Err(io::Error::new(
ErrorKind::Other,
"symlink is not supported by the system",
));
}
pub fn symlink_dir(src: &Path, dst: &Path) -> io::Result<()> {
#[cfg(windows)]
return std::os::windows::fs::symlink_dir(src, dst);
#[cfg(not(windows))]
symlink_file(src, dst)
}
pub fn strip_unc_prefix(path: &Path) -> &Path {
path.strip_prefix(r"\\?\").unwrap_or(path)
}
pub fn absolute(path: impl AsRef<Path>) -> io::Result<PathBuf> {
let path = path.as_ref();
let path = if path.is_absolute() {
path.to_path_buf()
} else {
std::env::current_dir()?.join(path)
};
if !path.is_absolute() {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("cannot get absolute path from {:?}", path),
));
}
Ok(normalize(&path))
}
pub fn normalize(path: &Path) -> PathBuf {
let mut result = PathBuf::new();
let mut poppable: usize = 0;
let mut has_root = false;
for component in path.components() {
match component {
Component::Normal(_) => {
poppable += 1;
result.push(component);
}
Component::RootDir => {
has_root = true;
result.push(component);
}
Component::Prefix(_) => {
result.push(component);
}
Component::ParentDir => {
if poppable > 0 {
result.pop();
poppable -= 1;
} else if !has_root {
result.push(component);
}
}
Component::CurDir => {}
}
}
if result.as_os_str().is_empty() {
return ".".into();
}
result
}
pub fn root_relative_path(root: &Path, cwd: &Path, path: &Path) -> io::Result<Option<PathBuf>> {
let path = normalize(&root.join(cwd).join(path));
if let Ok(suffix) = path.strip_prefix(root) {
return Ok(Some(suffix.to_path_buf()));
}
let root = root.canonicalize().path_context("canonicalizing", root)?;
let mut test = PathBuf::new();
let mut path_parts = path.components();
while let Some(part) = path_parts.next() {
test.push(part);
if test.is_symlink() {
test = test.canonicalize().path_context("canonicalizing", &test)?;
}
if let Ok(suffix) = test.strip_prefix(&root) {
return Ok(Some(suffix.join(path_parts)));
}
}
Ok(None)
}
pub fn remove_file<P: AsRef<Path>>(path: P) -> io::Result<()> {
let path = path.as_ref();
let path: Cow<Path> = if cfg!(windows) {
let tmp_path = path.with_extension(format!("tmp.{:x}", rand::random::<u16>()));
fs::rename(path, &tmp_path)?;
Cow::Owned(tmp_path)
} else {
Cow::Borrowed(path)
};
#[allow(clippy::needless_borrows_for_generic_args)]
let result = fs_remove_file(&path);
#[cfg(windows)]
match &result {
Err(e) if e.kind() == io::ErrorKind::PermissionDenied => {
if let Ok(r) = remove_directory_symlink(&path) {
if r {
return Ok(());
}
}
if windows_remove_mmap_file(&path).is_ok() {
return Ok(());
}
}
_ => {}
}
result.map_err(Into::into)
}
#[cfg(windows)]
fn remove_directory_symlink(path: &Path) -> io::Result<bool> {
let metadata = path.symlink_metadata()?;
if metadata.is_symlink() {
std::fs::remove_dir(path)?;
return Ok(true);
}
Ok(false)
}
#[cfg(windows)]
fn windows_remove_mmap_file(path: &Path) -> io::Result<()> {
use std::os::windows::ffi::OsStrExt;
use winapi::shared::minwindef::FALSE;
use winapi::um::fileapi::CreateFileW;
use winapi::um::fileapi::OPEN_EXISTING;
use winapi::um::handleapi::CloseHandle;
use winapi::um::handleapi::INVALID_HANDLE_VALUE;
use winapi::um::winbase::FILE_FLAG_DELETE_ON_CLOSE;
use winapi::um::winnt::DELETE;
use winapi::um::winnt::FILE_SHARE_DELETE;
use winapi::um::winnt::FILE_SHARE_READ;
use winapi::um::winnt::FILE_SHARE_WRITE;
let wpath: Vec<u16> = path.as_os_str().encode_wide().chain(Some(0)).collect();
let handle = unsafe {
CreateFileW(
wpath.as_ptr(),
DELETE,
FILE_SHARE_DELETE | FILE_SHARE_READ | FILE_SHARE_WRITE,
std::ptr::null_mut(),
OPEN_EXISTING,
FILE_FLAG_DELETE_ON_CLOSE,
std::ptr::null_mut(),
)
};
if handle == INVALID_HANDLE_VALUE {
let err = io::Error::last_os_error();
if err.kind() == io::ErrorKind::NotFound {
Ok(())
} else {
Err(err)
}
} else if unsafe { CloseHandle(handle) } == FALSE {
Err(io::Error::last_os_error())
} else {
Ok(())
}
}
#[cfg(unix)]
fn add_stat_context<T, E: Into<anyhow::Error>>(
res: anyhow::Result<T, E>,
path: Option<&Path>,
) -> anyhow::Result<T, anyhow::Error> {
use std::os::unix::fs::MetadataExt;
let res = res.map_err(Into::into);
if let Some(path) = path {
if let Ok(md) = path.metadata() {
return res.context(format!(
"stat({:?}) = dev:{} ino:{} mode:0o{:o} uid:{} gid:{} mtime:{}",
path,
md.dev(),
md.ino(),
md.mode(),
md.uid(),
md.gid(),
md.mtime()
));
}
}
res
}
#[cfg(unix)]
fn resolve_symlinks(path: &Path) -> anyhow::Result<PathBuf> {
fn inner(path: PathBuf, seen: &mut HashSet<PathBuf>) -> anyhow::Result<PathBuf> {
if seen.contains(&path) {
bail!("symlink cycle containing {:?}", path);
}
seen.insert(path.clone());
match path.read_link() {
Ok(target) => inner(target, seen),
Err(err) if err.kind() == io::ErrorKind::InvalidInput => Ok(path),
Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(path),
Err(err) => add_stat_context(
Err(err).context(format!("statting {:?}", path)),
path.parent(),
),
}
}
let mut seen = HashSet::new();
let mut res = inner(path.to_path_buf(), &mut seen);
if seen.len() > 1 {
res = res.with_context(|| format!("traversing symlinks from {:?}", path));
}
res
}
#[cfg(unix)]
#[context("creating dir {:?} with mode 0o{:o}", path, mode)]
fn create_dir_with_mode(path: &Path, mode: u32) -> anyhow::Result<()> {
use anyhow::anyhow;
let path = resolve_symlinks(path)?;
match path.metadata() {
Ok(md) if md.is_file() => {
return Err(anyhow!(io::Error::from(ErrorKind::AlreadyExists)))
.context(format!("path exists as a file: {:?}", path));
}
Ok(md) => {
if md.permissions().mode() & mode != mode {
let _ = fs::set_permissions(path, fs::Permissions::from_mode(mode));
}
return Ok(());
}
Err(err) if err.kind() == io::ErrorKind::NotFound => {}
Err(err) => {
return add_stat_context(
Err(err).context(format!("error statting {:?}", path)),
path.parent(),
);
}
};
let parent = path.parent().ok_or_else(|| {
io::Error::new(
ErrorKind::NotFound,
format!("`{:?}` does not have a parent directory", path),
)
})?;
let parent = resolve_symlinks(parent)?;
let temp = add_stat_context(tempfile::TempDir::new_in(&parent), Some(&parent))
.with_context(|| format!("creating temp dir in {:?}", parent))?;
fs::set_permissions(&temp, fs::Permissions::from_mode(mode))
.with_context(|| format!("setting permissions on temp dir {:?}", temp))?;
let temp = temp.into_path();
if let Err(e) = fs::rename(&temp, &path) {
let _ = fs::remove_dir(&temp);
match e.raw_os_error() {
Some(libc::ENOTEMPTY) | Some(libc::ENOTDIR) => {
Err(io::Error::from(ErrorKind::AlreadyExists).into())
}
_ => Err::<(), anyhow::Error>(e.into())
.context(format!("renaming temp dir {:?} to {:?}", temp, &path)),
}
} else {
Ok(())
}
}
#[cfg(not(unix))]
#[context("creating dir {:?}", path)]
fn create_dir_with_mode(path: &Path, _mode: u32) -> anyhow::Result<()> {
match fs::create_dir(path) {
Ok(()) => Ok(()),
Err(err) if err.kind() == io::ErrorKind::AlreadyExists && path.is_dir() => Ok(()),
Err(err) => Err(err.into()),
}
}
fn is_io_error_kind(err: &anyhow::Error, kind: ErrorKind) -> bool {
err.downcast_ref::<io::Error>()
.is_some_and(|err| err.kind() == kind)
}
pub fn create_dir(path: impl AsRef<Path>) -> anyhow::Result<()> {
create_dir_with_mode(path.as_ref(), 0o755)
}
pub fn create_shared_dir(path: impl AsRef<Path>) -> anyhow::Result<()> {
create_dir_with_mode(path.as_ref(), 0o2775)
}
pub fn create_dir_all_with_mode(path: impl AsRef<Path>, mode: u32) -> anyhow::Result<()> {
let mut to_create = vec![path.as_ref()];
while let Some(dir) = to_create.pop() {
match create_dir_with_mode(dir, mode) {
Ok(()) => continue,
Err(err) if is_io_error_kind(&err, io::ErrorKind::NotFound) => {
to_create.push(dir);
match dir.parent() {
Some(parent) => to_create.push(parent),
None => return Err(err),
}
}
Err(err) => return Err(err),
}
}
Ok(())
}
pub fn create_shared_dir_all(path: impl AsRef<Path>) -> anyhow::Result<()> {
create_dir_all_with_mode(path, 0o2775)
}
pub fn expand_path(path: impl AsRef<str>) -> PathBuf {
expand_path_impl(path.as_ref(), |k| env::var(k).ok(), dirs::home_dir)
}
fn expand_path_impl<E, H>(path: &str, getenv: E, homedir: H) -> PathBuf
where
E: FnMut(&str) -> Option<String>,
H: FnOnce() -> Option<PathBuf>,
{
let path = {
let mut new_path = String::new();
let mut is_starting = true;
for ch in path.chars() {
if ch == '%' {
if is_starting {
new_path.push_str("${");
} else {
new_path.push('}');
}
is_starting = !is_starting;
} else if cfg!(windows) && ch == '/' {
new_path.push('\\')
} else {
new_path.push(ch);
}
}
new_path
};
let path = shellexpand::env_with_context_no_errors(&path, getenv);
shellexpand::tilde_with_context(&path, homedir)
.as_ref()
.into()
}
pub fn relativize(base: &Path, path: &Path) -> PathBuf {
let mut base_iter = base.iter();
let mut path_iter = path.iter();
let mut rel_path = PathBuf::new();
loop {
match (base_iter.next(), path_iter.next()) {
(Some(ref c), Some(ref p)) if c == p => continue,
(Some(_c), remaining_path) => {
rel_path.push(".."); for _ in base_iter {
rel_path.push("..");
}
if let Some(p) = remaining_path {
rel_path.push(p);
for component in path_iter {
rel_path.push(component);
}
}
break;
}
(None, Some(p)) => {
rel_path.push(p);
for component in path_iter {
rel_path.push(component);
}
break;
}
(None, None) => {
break;
}
}
}
rel_path
}
#[cfg(windows)]
pub fn replace_slash_with_backslash(path: &Path) -> PathBuf {
use std::os::windows::ffi::OsStrExt;
use std::os::windows::ffi::OsStringExt;
use widestring::U16Str;
let mut utf16_string: Vec<u16> = path.as_os_str().encode_wide().collect();
let to_replace = U16Str::from_slice(&mut utf16_string)
.char_indices()
.filter_map(|(i, c)| {
c.ok()
.map(|c| if c == '/' { Some(i) } else { None })
.flatten()
})
.collect::<Vec<_>>();
for i in to_replace {
utf16_string[i] = '\\' as u16;
}
PathBuf::from(std::ffi::OsString::from_wide(&utf16_string))
}
#[cfg(test)]
mod tests {
use std::fs::create_dir_all;
use std::fs::File;
use anyhow::Result;
use tempfile::TempDir;
use super::*;
#[cfg(windows)]
mod windows {
use std::os::windows::ffi::OsStrExt;
use std::os::windows::ffi::OsStringExt;
use super::*;
#[test]
fn test_absolute_fullpath() {
assert_eq!(absolute("C:/foo").unwrap(), Path::new("C:\\foo"));
assert_eq!(
absolute("x:\\a/b\\./.\\c").unwrap(),
Path::new("x:\\a\\b\\c")
);
assert_eq!(
absolute("y:/a/b\\../..\\c\\../d\\./.").unwrap(),
Path::new("y:\\d")
);
assert_eq!(
absolute("z:/a/b\\../..\\../..\\..").unwrap(),
Path::new("z:\\")
);
}
#[test]
fn test_normalize_path() {
assert_eq!(normalize(r"a/b\c\..\.".as_ref()), Path::new(r"a\b"));
assert_eq!(normalize("z:/a//b/./".as_ref()), Path::new(r"z:\a\b"));
}
#[test]
fn test_replace_slash_with_backslash() {
let utf16_bytes: &[u16] = &[0xd83c, 0xdf31, 0x002f, 0x0073, 0x0061, 0x0070];
let path = PathBuf::from(std::ffi::OsString::from_wide(utf16_bytes));
let expected = "🌱\\sap".encode_utf16().collect::<Vec<_>>();
assert_eq!(
replace_slash_with_backslash(&path)
.as_os_str()
.encode_wide()
.collect::<Vec<_>>(),
expected,
);
let utf16_bytes: &[u16] = &[0xd83c, 0x002f, 0x002f, 0x0073, 0x0061, 0x0070];
let expected: Vec<u16> = Vec::from([0xd83c, 0x005c, 0x005c, 0x0073, 0x0061, 0x0070]);
let path = PathBuf::from(std::ffi::OsString::from_wide(utf16_bytes));
assert_eq!(
replace_slash_with_backslash(&path)
.as_os_str()
.encode_wide()
.collect::<Vec<_>>(),
expected,
);
let utf16_bytes: &[u16] = &[0x002f, 0xdf31, 0x002f, 0x0073, 0x0061, 0x0070];
let expected: Vec<u16> = Vec::from([0x005c, 0xdf31, 0x005c, 0x0073, 0x0061, 0x0070]);
let path = PathBuf::from(std::ffi::OsString::from_wide(utf16_bytes));
assert_eq!(
replace_slash_with_backslash(&path)
.as_os_str()
.encode_wide()
.collect::<Vec<_>>(),
expected,
);
}
}
#[cfg(unix)]
mod unix {
use super::*;
#[test]
fn test_absolute_fullpath() {
assert_eq!(absolute("/a/./b\\c/../d/.").unwrap(), Path::new("/a/d"));
assert_eq!(absolute("/a/../../../../b").unwrap(), Path::new("/b"));
assert_eq!(absolute("/../../..").unwrap(), Path::new("/"));
assert_eq!(absolute("/../../../").unwrap(), Path::new("/"));
assert_eq!(
absolute("//foo///bar//baz").unwrap(),
Path::new("/foo/bar/baz")
);
assert_eq!(absolute("//").unwrap(), Path::new("/"));
}
#[test]
fn test_normalize_path() {
assert_eq!(normalize("/a/./b/../d/.".as_ref()), Path::new("/a/d"));
assert_eq!(normalize("./a/b/c/../../".as_ref()), Path::new("a"));
assert_eq!(normalize("".as_ref()), Path::new("."));
assert_eq!(normalize(".".as_ref()), Path::new("."));
assert_eq!(normalize("..".as_ref()), Path::new(".."));
assert_eq!(normalize("/..".as_ref()), Path::new("/"));
assert_eq!(normalize("/../..".as_ref()), Path::new("/"));
assert_eq!(normalize("./..".as_ref()), Path::new(".."));
assert_eq!(normalize("../../..".as_ref()), Path::new("../../.."));
assert_eq!(normalize("////".as_ref()), Path::new("/"));
}
#[test]
fn test_create_dir_mode() -> Result<()> {
let tempdir = TempDir::new()?;
let mut path = tempdir.path().to_path_buf();
path.push("dir");
create_dir(&path)?;
assert!(path.is_dir());
let metadata = path.metadata()?;
assert_eq!(metadata.permissions().mode(), 0o40755);
assert_eq!(tempdir.path().read_dir()?.count(), 1);
Ok(())
}
#[test]
fn test_create_shared_dir() -> Result<()> {
let tempdir = TempDir::new()?;
let mut path = tempdir.path().to_path_buf();
path.push("shared");
create_shared_dir(&path)?;
assert!(path.is_dir());
let metadata = path.metadata()?;
assert_eq!(metadata.permissions().mode(), 0o42775);
assert_eq!(tempdir.path().read_dir()?.count(), 1);
Ok(())
}
#[test]
fn test_fixup_perms() -> Result<()> {
let tempdir = TempDir::new()?;
let mut path = tempdir.path().to_path_buf();
path.push("shared");
create_dir_with_mode(&path, 0o775)?;
let metadata = path.metadata()?;
assert_eq!(metadata.permissions().mode(), 0o40775);
create_shared_dir(&path)?;
let metadata = path.metadata()?;
assert_eq!(metadata.permissions().mode(), 0o42775);
Ok(())
}
#[test]
fn test_create_dir_no_perms() -> Result<()> {
let tempdir = TempDir::new()?;
let mut path = tempdir.path().to_path_buf();
path.push("nope");
std::fs::create_dir(&path)?;
std::fs::set_permissions(&path, fs::Permissions::from_mode(0o0))?;
let err = create_dir_with_mode(&path.join("dir"), 0o775).unwrap_err();
assert!(is_io_error_kind(&err, io::ErrorKind::PermissionDenied));
assert!(format!("{:?}", err).contains(&format!("stat({:?}) = ", path)));
Ok(())
}
}
fn test_create_dir_all_fn(
create_fn: &dyn Fn(&PathBuf) -> anyhow::Result<()>,
mode: u32,
) -> Result<()> {
let tempdir = TempDir::new()?;
let path = tempdir.path().join("foo").join("bar");
create_fn(&path)?;
assert!(path.is_dir());
#[cfg(unix)]
{
let metadata = path.metadata()?;
assert_eq!(metadata.permissions().mode(), mode);
}
#[cfg(windows)]
let _ = mode;
create_fn(&path)?;
#[cfg(unix)]
{
let broken_symlink = tempdir.path().join("foo").join("bar").join("oops");
symlink_file(&tempdir.path().join("doesnt_exist"), &broken_symlink)?;
assert!(create_fn(&broken_symlink.join("nope")).is_ok());
assert!(tempdir.path().join("doesnt_exist").join("nope").is_dir());
}
let regular_file = tempdir.path().join("regular_file");
File::create(®ular_file)?;
assert!(create_fn(®ular_file).is_err());
assert!(create_fn(®ular_file.join("no_can_do")).is_err());
Ok(())
}
#[test]
#[cfg_attr(windows, ignore)] fn test_atomic_write_symlink() -> Result<()> {
let dir = tempfile::tempdir().unwrap();
let path = dir.path();
let j = |name| -> PathBuf { path.join(name) };
fs::write(j("a"), b"1")?;
atomic_write_symlink(&j("c"), b"2")?;
let file = fs::OpenOptions::new().read(true).open(j("c"))?;
let mmap = unsafe { memmap2::Mmap::map(&file) }?;
fs::write(j("c.aaaa.atomic"), b"0")?;
atomic_write_symlink(&j("c"), b"3")?;
assert_eq!(mmap.as_ref(), b"2");
assert_eq!(fs::read(j("c"))?, b"3");
assert!(j("a").exists());
let count = || {
fs::read_dir(path)
.unwrap()
.filter(|e| e.as_ref().unwrap().path().exists())
.count()
};
assert_eq!(count(), 4);
atomic_write_symlink(&j("a"), b"4")?;
atomic_write_symlink(&j("a"), b"5")?;
atomic_write_symlink(&j("a"), b"6")?;
assert_eq!(count(), 6);
Ok(())
}
#[test]
fn test_mmap_delete() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("foo");
fs::write(&path, b"bar").unwrap();
let file = fs::OpenOptions::new().read(true).open(&path).unwrap();
let _mmap = unsafe { memmap2::Mmap::map(&file) }.unwrap();
remove_file(&path).unwrap();
assert!(!path.exists());
fs::write(&path, b"baz").unwrap();
}
#[test]
fn test_create_dir_non_exist() -> Result<()> {
let tempdir = TempDir::new()?;
let mut path = tempdir.path().to_path_buf();
path.push("dir");
create_dir(&path)?;
assert!(path.is_dir());
Ok(())
}
#[test]
fn test_create_dir_exist() -> Result<()> {
let tempdir = TempDir::new()?;
let mut path = tempdir.path().to_path_buf();
path.push("dir");
create_dir(&path)?;
assert!(&path.is_dir());
create_dir(&path)?;
assert!(&path.is_dir());
Ok(())
}
#[test]
fn test_create_dir_file_exist() -> Result<()> {
let tempdir = TempDir::new()?;
let mut path = tempdir.path().to_path_buf();
path.push("dir");
File::create(&path)?;
let err = create_dir(&path).unwrap_err();
assert!(is_io_error_kind(&err, io::ErrorKind::AlreadyExists));
Ok(())
}
#[test]
fn test_create_dir_with_nonexistent_parent() -> Result<()> {
let tempdir = TempDir::new()?;
let mut path = tempdir.path().to_path_buf();
path.push("nonexistentparent");
path.push("dir");
let err = create_dir(&path).unwrap_err();
assert!(is_io_error_kind(&err, ErrorKind::NotFound));
Ok(())
}
#[test]
fn test_create_dir_without_empty_path() {
let empty = Path::new("");
let err = create_dir(empty).unwrap_err();
assert!(is_io_error_kind(&err, ErrorKind::NotFound));
}
#[test]
fn test_path_expansion() {
fn getenv(key: &str) -> Option<String> {
match key {
"foo" => Some("~/a".into()),
"bar" => Some("b".into()),
_ => None,
}
}
fn homedir() -> Option<PathBuf> {
Some(PathBuf::from("/home/user"))
}
let path = "$foo/${bar}/$baz";
let expected = PathBuf::from("/home/user/a/b/$baz");
assert_eq!(expand_path_impl(path, getenv, homedir), expected);
}
#[test]
fn test_create_shared_dir_all() -> Result<()> {
test_create_dir_all_fn(&|path| create_shared_dir_all(path), 0o42775)
}
#[test]
fn test_create_dir_all_with_mode() -> Result<()> {
test_create_dir_all_fn(&|path| create_dir_all_with_mode(path, 0o777), 0o40777)
}
#[test]
fn test_relativize_absolute_paths() {
let check = |base, path, expected| {
assert_eq!(
relativize(Path::new(base), Path::new(path)),
Path::new(expected)
);
};
check("/", "/", "");
check("/foo/bar/baz", "/foo/bar/baz", "");
check("/foo/bar", "/foo/bar/baz", "baz");
check("/foo", "/foo/bar/baz", "bar/baz");
check("/foo/bar/baz", "/foo/bar", "..");
check("/foo/bar/baz", "/foo", "../..");
check("/foo/bar/baz", "/foo/BAR", "../../BAR");
check("/foo/bar/baz", "/foo/BAR/BAZ", "../../BAR/BAZ");
}
#[test]
fn test_relativize_platform_absolute_paths() {
let cwd = Path::new(".").canonicalize().unwrap();
let result = relativize(&cwd, &cwd.join("a").join("b"));
assert_eq!(result, Path::new("a").join("b"));
}
#[test]
fn test_root_relative_path() -> Result<()> {
let tempdir = TempDir::new()?;
let parent = tempdir.path().join("parent");
let root = parent.join("root");
let child = root.join("child");
create_dir_all(&child)?;
assert_eq!(
root_relative_path(&root, &root, ".".as_ref())?,
Some(PathBuf::from(""))
);
assert_eq!(
root_relative_path(&root, &child, ".".as_ref())?,
Some(PathBuf::from("child"))
);
assert_eq!(
root_relative_path(&root, &child, "foo".as_ref())?,
Some(["child", "foo"].iter().collect::<PathBuf>()),
);
let symlink_to_root = parent.join("symlink_to_root");
symlink_dir(&root, &symlink_to_root)?;
assert_eq!(
root_relative_path(&root, &root, &symlink_to_root.join("child"))?,
Some(PathBuf::from("child")),
);
let symlink_to_child = parent.join("symlink_to_child");
symlink_dir(&child, &symlink_to_child)?;
assert_eq!(
root_relative_path(&root, &root, &symlink_to_child.join("foo"))?,
Some(["child", "foo"].iter().collect::<PathBuf>()),
);
assert!(root_relative_path(&root, &parent, "foo".as_ref())?.is_none());
assert!(root_relative_path(&root, &root, &parent.join("rootbeer"))?.is_none());
Ok(())
}
}