use logical_path::LogicalPathContext;
#[cfg(unix)]
static ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(windows)]
static WIN_ENV_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(unix)]
struct EnvGuard {
saved_dir: std::path::PathBuf,
saved_pwd: Option<std::ffi::OsString>,
_lock: std::sync::MutexGuard<'static, ()>,
}
#[cfg(unix)]
impl EnvGuard {
fn acquire() -> Self {
let lock = ENV_MUTEX.lock().unwrap_or_else(|e| {
eprintln!("EnvGuard: recovering poisoned ENV_MUTEX after a previous test panic");
e.into_inner()
});
EnvGuard {
saved_dir: std::env::current_dir().expect("current_dir"),
saved_pwd: std::env::var_os("PWD"),
_lock: lock,
}
}
fn set(&self, canonical_dir: &std::path::Path, logical_pwd: &std::path::Path) {
std::env::set_current_dir(canonical_dir).expect("set_current_dir");
unsafe { std::env::set_var("PWD", logical_pwd) };
}
}
#[cfg(unix)]
impl Drop for EnvGuard {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.saved_dir);
match &self.saved_pwd {
Some(p) => unsafe { std::env::set_var("PWD", p) },
None => unsafe { std::env::remove_var("PWD") },
}
}
}
#[cfg(unix)]
#[test]
fn detect_inside_real_symlink_has_mapping() {
use std::os::unix::fs::symlink;
let guard = EnvGuard::acquire();
let dir = tempfile::tempdir().unwrap();
let base = std::fs::canonicalize(dir.path()).unwrap();
let real_dir = base.join("real").join("project");
let link_base = base.join("link");
std::fs::create_dir_all(&real_dir).unwrap();
symlink(base.join("real"), &link_base).unwrap();
guard.set(&real_dir, &link_base.join("project"));
let ctx = LogicalPathContext::detect();
assert!(ctx.has_mapping());
}
#[cfg(unix)]
#[test]
fn detect_nested_symlinks_detects_outermost_divergence() {
use std::os::unix::fs::symlink;
let guard = EnvGuard::acquire();
let dir = tempfile::tempdir().unwrap();
let base = std::fs::canonicalize(dir.path()).unwrap();
let real_dir = base.join("real").join("project");
let link1 = base.join("link1");
let link2 = base.join("link2");
std::fs::create_dir_all(&real_dir).unwrap();
symlink(base.join("real"), &link1).unwrap();
symlink(&link1, &link2).unwrap();
guard.set(&real_dir, &link2.join("project"));
let ctx = LogicalPathContext::detect();
assert!(ctx.has_mapping());
}
#[cfg(unix)]
#[test]
fn to_logical_with_real_symlink() {
use std::os::unix::fs::symlink;
let guard = EnvGuard::acquire();
let dir = tempfile::tempdir().unwrap();
let base = std::fs::canonicalize(dir.path()).unwrap();
let real_dir = base.join("real").join("src");
let link_base = base.join("link");
std::fs::create_dir_all(&real_dir).unwrap();
symlink(base.join("real"), &link_base).unwrap();
guard.set(&real_dir, &link_base.join("src"));
let ctx = LogicalPathContext::detect();
let canonical_path = base.join("real").join("src");
let result = ctx.to_logical(&canonical_path);
assert_eq!(result, link_base.join("src"));
}
#[cfg(unix)]
#[test]
fn to_canonical_with_real_symlink() {
use std::os::unix::fs::symlink;
let guard = EnvGuard::acquire();
let dir = tempfile::tempdir().unwrap();
let base = std::fs::canonicalize(dir.path()).unwrap();
let real_dir = base.join("real").join("src");
let link_base = base.join("link");
std::fs::create_dir_all(&real_dir).unwrap();
symlink(base.join("real"), &link_base).unwrap();
guard.set(&real_dir, &link_base.join("src"));
let ctx = LogicalPathContext::detect();
let logical_path = link_base.join("src");
let result = ctx.to_canonical(&logical_path);
assert_eq!(result, base.join("real").join("src"));
}
#[cfg(unix)]
#[test]
fn to_logical_idempotent_with_real_symlink() {
use std::os::unix::fs::symlink;
let guard = EnvGuard::acquire();
let dir = tempfile::tempdir().unwrap();
let base = std::fs::canonicalize(dir.path()).unwrap();
let real_dir = base.join("real").join("src");
let link_base = base.join("link");
std::fs::create_dir_all(&real_dir).unwrap();
symlink(base.join("real"), &link_base).unwrap();
guard.set(&real_dir, &link_base.join("src"));
let ctx = LogicalPathContext::detect();
let canonical = base.join("real").join("src");
let logical = ctx.to_logical(&canonical);
assert_eq!(logical, link_base.join("src"));
let again = ctx.to_logical(&logical);
assert_eq!(again, logical);
}
#[cfg(unix)]
#[test]
fn to_logical_translates_file_path_with_real_symlink() {
use std::os::unix::fs::symlink;
let guard = EnvGuard::acquire();
let dir = tempfile::tempdir().unwrap();
let base = std::fs::canonicalize(dir.path()).unwrap();
let real_dir = base.join("real").join("src");
let link_base = base.join("link");
std::fs::create_dir_all(&real_dir).unwrap();
std::fs::write(real_dir.join("main.rs"), b"fn main() {}").unwrap();
symlink(base.join("real"), &link_base).unwrap();
guard.set(&real_dir, &link_base.join("src"));
let ctx = LogicalPathContext::detect();
let canonical_file = base.join("real").join("src").join("main.rs");
let result = ctx.to_logical(&canonical_file);
assert_eq!(result, link_base.join("src").join("main.rs"));
}
#[cfg(target_os = "linux")]
#[test]
fn detect_with_real_symlink_on_linux() {
use std::os::unix::fs::symlink;
let guard = EnvGuard::acquire();
let dir = tempfile::tempdir().unwrap();
let base = std::fs::canonicalize(dir.path()).unwrap();
let real_dir = base.join("real").join("project");
let link_base = base.join("link");
std::fs::create_dir_all(&real_dir).unwrap();
symlink(base.join("real"), &link_base).unwrap();
guard.set(&real_dir, &link_base.join("project"));
let ctx = LogicalPathContext::detect();
assert!(ctx.has_mapping());
let canonical = base.join("real").join("project");
let logical = ctx.to_logical(&canonical);
assert_eq!(logical, link_base.join("project"));
}
#[cfg(windows)]
mod windows_helpers {
use std::path::{Path, PathBuf};
use std::process::Command;
pub fn strip_extended_prefix(p: PathBuf) -> PathBuf {
let s = match p.to_str() {
Some(s) => s,
None => return p,
};
if let Some(rest) = s.strip_prefix(r"\\?\") {
PathBuf::from(rest)
} else {
p
}
}
pub fn create_junction(link: &Path, target: &Path) -> std::io::Result<()> {
let output = Command::new("cmd")
.args(["/c", "mklink", "/J"])
.arg(link)
.arg(target)
.output()?;
if output.status.success() {
Ok(())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"mklink /J failed: {}",
String::from_utf8_lossy(&output.stderr)
),
))
}
}
pub fn create_dir_symlink(link: &Path, target: &Path) -> std::io::Result<()> {
let output = Command::new("cmd")
.args(["/c", "mklink", "/D"])
.arg(link)
.arg(target)
.output()?;
if output.status.success() {
Ok(())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"mklink /D failed: {}",
String::from_utf8_lossy(&output.stderr)
),
))
}
}
pub fn remove_junction(link: &Path) -> std::io::Result<()> {
let output = Command::new("cmd").args(["/c", "rd"]).arg(link).output()?;
if output.status.success() {
Ok(())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("rd failed: {}", String::from_utf8_lossy(&output.stderr)),
))
}
}
pub fn create_subst(letter: char, target: &Path) -> std::io::Result<()> {
let output = Command::new("subst")
.arg(format!("{letter}:"))
.arg(target)
.output()?;
if output.status.success() {
Ok(())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!("subst failed: {}", String::from_utf8_lossy(&output.stderr)),
))
}
}
pub fn remove_subst(letter: char) -> std::io::Result<()> {
let output = Command::new("subst")
.arg(format!("{letter}:"))
.arg("/D")
.output()?;
if output.status.success() {
Ok(())
} else {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"subst /D failed: {}",
String::from_utf8_lossy(&output.stderr)
),
))
}
}
pub struct WinEnvGuard {
_lock: std::sync::MutexGuard<'static, ()>,
saved_dir: PathBuf,
junctions: Vec<PathBuf>,
subst_drives: Vec<char>,
}
impl WinEnvGuard {
pub fn new() -> Self {
let lock = super::WIN_ENV_MUTEX.lock().unwrap_or_else(|e| {
eprintln!(
"WinEnvGuard: recovering poisoned WIN_ENV_MUTEX after a previous test panic"
);
e.into_inner()
});
WinEnvGuard {
_lock: lock,
saved_dir: std::env::current_dir().expect("current_dir"),
junctions: Vec::new(),
subst_drives: Vec::new(),
}
}
pub fn track_junction(&mut self, link: PathBuf) {
self.junctions.push(link);
}
pub fn track_subst(&mut self, letter: char) {
self.subst_drives.push(letter);
}
pub fn untrack_junction(&mut self, link: &PathBuf) {
self.junctions.retain(|j| j != link);
}
pub fn untrack_subst(&mut self, letter: char) {
self.subst_drives.retain(|&d| d != letter);
}
pub fn set_cwd(&self, dir: &Path) {
std::env::set_current_dir(dir).expect("set_current_dir");
}
}
impl Drop for WinEnvGuard {
fn drop(&mut self) {
let _ = std::env::set_current_dir(&self.saved_dir);
for junction in &self.junctions {
let _ = remove_junction(junction);
}
for &letter in &self.subst_drives {
let _ = remove_subst(letter);
}
}
}
pub fn find_unused_drive_letter() -> Option<char> {
('D'..='Z')
.rev()
.find(|&c| !std::path::Path::new(&format!("{c}:\\")).exists())
}
}
#[cfg(windows)]
#[test]
fn detect_junction_has_mapping() {
use windows_helpers::*;
let dir = tempfile::tempdir().unwrap();
let base = strip_extended_prefix(std::fs::canonicalize(dir.path()).unwrap());
let real_dir = base.join("real");
let link_dir = base.join("link");
std::fs::create_dir_all(&real_dir).unwrap();
let mut guard = WinEnvGuard::new();
create_junction(&link_dir, &real_dir).expect("mklink /J");
guard.track_junction(link_dir.clone());
guard.set_cwd(&link_dir);
let ctx = LogicalPathContext::detect();
assert!(ctx.has_mapping());
}
#[cfg(windows)]
#[test]
fn detect_junction_to_logical() {
use windows_helpers::*;
let dir = tempfile::tempdir().unwrap();
let base = strip_extended_prefix(std::fs::canonicalize(dir.path()).unwrap());
let real_dir = base.join("real");
let link_dir = base.join("link");
let subdir = real_dir.join("src");
std::fs::create_dir_all(&subdir).unwrap();
let mut guard = WinEnvGuard::new();
create_junction(&link_dir, &real_dir).expect("mklink /J");
guard.track_junction(link_dir.clone());
guard.set_cwd(&link_dir);
let ctx = LogicalPathContext::detect();
let canonical = real_dir.join("src");
let result = ctx.to_logical(&canonical);
assert_eq!(result, link_dir.join("src"));
}
#[cfg(windows)]
#[test]
fn detect_no_junction_no_mapping() {
use windows_helpers::*;
let dir = tempfile::tempdir().unwrap();
let base = strip_extended_prefix(std::fs::canonicalize(dir.path()).unwrap());
let guard = WinEnvGuard::new();
guard.set_cwd(&base);
let ctx = LogicalPathContext::detect();
assert!(!ctx.has_mapping());
}
#[cfg(windows)]
#[test]
fn detect_junction_removed_fallback() {
use windows_helpers::*;
let dir = tempfile::tempdir().unwrap();
let base = strip_extended_prefix(std::fs::canonicalize(dir.path()).unwrap());
let real_dir = base.join("real");
let link_dir = base.join("link");
let subdir = real_dir.join("src");
std::fs::create_dir_all(&subdir).unwrap();
let mut guard = WinEnvGuard::new();
create_junction(&link_dir, &real_dir).expect("mklink /J");
guard.track_junction(link_dir.clone());
guard.set_cwd(&link_dir);
let ctx = LogicalPathContext::detect();
guard.set_cwd(&base);
remove_junction(&link_dir).expect("remove junction");
guard.untrack_junction(&link_dir);
let canonical = real_dir.join("src");
let result = ctx.to_logical(&canonical);
assert_eq!(result, canonical);
}
#[cfg(windows)]
#[test]
fn detect_dir_symlink_has_mapping() {
use windows_helpers::*;
let dir = tempfile::tempdir().unwrap();
let base = strip_extended_prefix(std::fs::canonicalize(dir.path()).unwrap());
let real_dir = base.join("real");
let link_dir = base.join("symlink");
std::fs::create_dir_all(&real_dir).unwrap();
let mut guard = WinEnvGuard::new();
if create_dir_symlink(&link_dir, &real_dir).is_err() {
return;
}
guard.track_junction(link_dir.clone()); guard.set_cwd(&link_dir);
let ctx = LogicalPathContext::detect();
assert!(ctx.has_mapping());
let canonical = real_dir.join("file.txt");
std::fs::write(&canonical, b"test").unwrap();
let result = ctx.to_logical(&canonical);
assert_eq!(result, link_dir.join("file.txt"));
}
#[cfg(windows)]
#[test]
fn junction_roundtrip() {
use windows_helpers::*;
let dir = tempfile::tempdir().unwrap();
let base = strip_extended_prefix(std::fs::canonicalize(dir.path()).unwrap());
let real_dir = base.join("real");
let link_dir = base.join("link");
let subdir = real_dir.join("src");
std::fs::create_dir_all(&subdir).unwrap();
let mut guard = WinEnvGuard::new();
create_junction(&link_dir, &real_dir).expect("mklink /J");
guard.track_junction(link_dir.clone());
guard.set_cwd(&link_dir);
let ctx = LogicalPathContext::detect();
let canonical = real_dir.join("src");
let logical = ctx.to_logical(&canonical);
let back = ctx.to_canonical(&logical);
assert_eq!(back, canonical);
}
#[cfg(windows)]
#[test]
fn detect_subst_has_mapping() {
use windows_helpers::*;
let dir = tempfile::tempdir().unwrap();
let base = strip_extended_prefix(std::fs::canonicalize(dir.path()).unwrap());
let letter =
find_unused_drive_letter().expect("no unused drive letter available for subst test");
let mut guard = WinEnvGuard::new();
create_subst(letter, &base).expect("subst");
guard.track_subst(letter);
guard.set_cwd(std::path::Path::new(&format!("{letter}:\\")));
let ctx = LogicalPathContext::detect();
assert!(ctx.has_mapping());
}
#[cfg(windows)]
#[test]
fn detect_subst_to_logical() {
use windows_helpers::*;
let dir = tempfile::tempdir().unwrap();
let base = strip_extended_prefix(std::fs::canonicalize(dir.path()).unwrap());
let subdir = base.join("src");
std::fs::create_dir_all(&subdir).unwrap();
let letter =
find_unused_drive_letter().expect("no unused drive letter available for subst test");
let mut guard = WinEnvGuard::new();
create_subst(letter, &base).expect("subst");
guard.track_subst(letter);
guard.set_cwd(std::path::Path::new(&format!("{letter}:\\")));
let ctx = LogicalPathContext::detect();
let canonical = subdir;
let result = ctx.to_logical(&canonical);
assert_eq!(result, std::path::PathBuf::from(format!(r"{letter}:\src")));
}
#[cfg(windows)]
#[test]
fn subst_to_logical_outside_mapping() {
use windows_helpers::*;
let dir = tempfile::tempdir().unwrap();
let base = strip_extended_prefix(std::fs::canonicalize(dir.path()).unwrap());
let letter =
find_unused_drive_letter().expect("no unused drive letter available for subst test");
let mut guard = WinEnvGuard::new();
create_subst(letter, &base).expect("subst");
guard.track_subst(letter);
guard.set_cwd(std::path::Path::new(&format!("{letter}:\\")));
let ctx = LogicalPathContext::detect();
let outside = std::path::Path::new(r"C:\Windows\System32");
assert_eq!(ctx.to_logical(outside), outside.to_path_buf());
}
#[cfg(windows)]
#[test]
fn subst_removed_fallback() {
use windows_helpers::*;
let dir = tempfile::tempdir().unwrap();
let base = strip_extended_prefix(std::fs::canonicalize(dir.path()).unwrap());
let subdir = base.join("src");
std::fs::create_dir_all(&subdir).unwrap();
let letter =
find_unused_drive_letter().expect("no unused drive letter available for subst test");
let mut guard = WinEnvGuard::new();
create_subst(letter, &base).expect("subst");
guard.track_subst(letter);
guard.set_cwd(std::path::Path::new(&format!("{letter}:\\")));
let ctx = LogicalPathContext::detect();
guard.set_cwd(&base);
let _ = remove_subst(letter);
guard.untrack_subst(letter);
let result = ctx.to_logical(&subdir);
assert_eq!(result, subdir);
}
#[cfg(windows)]
#[test]
fn subst_roundtrip() {
use windows_helpers::*;
let dir = tempfile::tempdir().unwrap();
let base = strip_extended_prefix(std::fs::canonicalize(dir.path()).unwrap());
let subdir = base.join("src");
std::fs::create_dir_all(&subdir).unwrap();
let letter =
find_unused_drive_letter().expect("no unused drive letter available for subst test");
let mut guard = WinEnvGuard::new();
create_subst(letter, &base).expect("subst");
guard.track_subst(letter);
guard.set_cwd(std::path::Path::new(&format!("{letter}:\\")));
let ctx = LogicalPathContext::detect();
let canonical = subdir;
let logical = ctx.to_logical(&canonical);
let back = ctx.to_canonical(&logical);
assert_eq!(back, canonical);
}
#[cfg(windows)]
#[test]
fn junction_retarget_fallback() {
use windows_helpers::*;
let dir = tempfile::tempdir().unwrap();
let base = strip_extended_prefix(std::fs::canonicalize(dir.path()).unwrap());
let real_dir1 = base.join("real1");
let real_dir2 = base.join("real2");
let link_dir = base.join("link");
let subdir = real_dir1.join("src");
std::fs::create_dir_all(&subdir).unwrap();
std::fs::create_dir_all(real_dir2.join("src")).unwrap();
let mut guard = WinEnvGuard::new();
create_junction(&link_dir, &real_dir1).expect("mklink /J");
guard.track_junction(link_dir.clone());
guard.set_cwd(&link_dir);
let ctx = LogicalPathContext::detect();
guard.set_cwd(&base);
remove_junction(&link_dir).expect("remove junction");
create_junction(&link_dir, &real_dir2).expect("mklink /J new target");
let canonical = real_dir1.join("src");
let result = ctx.to_logical(&canonical);
assert_eq!(result, canonical);
}
#[cfg(windows)]
#[test]
fn junction_to_canonical() {
use windows_helpers::*;
let dir = tempfile::tempdir().unwrap();
let base = strip_extended_prefix(std::fs::canonicalize(dir.path()).unwrap());
let real_dir = base.join("real");
let link_dir = base.join("link");
let subdir = real_dir.join("src");
std::fs::create_dir_all(&subdir).unwrap();
let mut guard = WinEnvGuard::new();
create_junction(&link_dir, &real_dir).expect("mklink /J");
guard.track_junction(link_dir.clone());
guard.set_cwd(&link_dir);
let ctx = LogicalPathContext::detect();
let logical = link_dir.join("src");
let result = ctx.to_canonical(&logical);
assert_eq!(result, real_dir.join("src"));
}
#[cfg(windows)]
#[test]
fn junction_to_canonical_outside_prefix() {
use windows_helpers::*;
let dir = tempfile::tempdir().unwrap();
let base = strip_extended_prefix(std::fs::canonicalize(dir.path()).unwrap());
let real_dir = base.join("real");
let link_dir = base.join("link");
std::fs::create_dir_all(&real_dir).unwrap();
let mut guard = WinEnvGuard::new();
create_junction(&link_dir, &real_dir).expect("mklink /J");
guard.track_junction(link_dir.clone());
guard.set_cwd(&link_dir);
let ctx = LogicalPathContext::detect();
let outside = std::path::Path::new(r"C:\Windows\System32");
assert_eq!(ctx.to_canonical(outside), outside.to_path_buf());
}
#[cfg(windows)]
#[test]
fn no_mapping_to_canonical_unchanged() {
use windows_helpers::*;
let dir = tempfile::tempdir().unwrap();
let base = strip_extended_prefix(std::fs::canonicalize(dir.path()).unwrap());
let guard = WinEnvGuard::new();
guard.set_cwd(&base);
let ctx = LogicalPathContext::detect();
let input = std::path::Path::new(r"C:\Users\dev\project\src\main.rs");
assert_eq!(ctx.to_canonical(input), input.to_path_buf());
}