#![deny(missing_docs)]
#[cfg(not(windows))]
use std::ffi::OsStr;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LogicalPathContext {
mapping: Option<PrefixMapping>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct PrefixMapping {
canonical_prefix: PathBuf,
logical_prefix: PathBuf,
}
impl Default for LogicalPathContext {
fn default() -> Self {
LogicalPathContext { mapping: None }
}
}
impl LogicalPathContext {
#[must_use]
pub fn detect() -> LogicalPathContext {
#[cfg(windows)]
{
let cwd = match std::env::current_dir() {
Ok(cwd) => cwd,
Err(e) => {
log::debug!("detect: current_dir() failed: {e}");
return LogicalPathContext { mapping: None };
}
};
let canonical_cwd = match std::fs::canonicalize(&cwd) {
Ok(c) => strip_extended_length_prefix(&c),
Err(e) => {
log::debug!("detect: canonicalize({}) failed: {e}", cwd.display());
return LogicalPathContext { mapping: None };
}
};
log::trace!(
"detect (Windows): cwd={}, canonical_cwd={}",
cwd.display(),
canonical_cwd.display()
);
Self::detect_from_cwd(&cwd, &canonical_cwd)
}
#[cfg(not(windows))]
{
let pwd = std::env::var_os("PWD");
let canonical_cwd = match std::env::current_dir() {
Ok(cwd) => cwd,
Err(e) => {
log::debug!("detect: current_dir() failed: {e}");
return LogicalPathContext { mapping: None };
}
};
log::trace!(
"detect (Unix): PWD={:?}, canonical_cwd={}",
pwd,
canonical_cwd.display()
);
Self::detect_from(pwd.as_deref(), &canonical_cwd)
}
}
#[cfg(not(windows))]
pub(crate) fn detect_from(pwd: Option<&OsStr>, canonical_cwd: &Path) -> LogicalPathContext {
let pwd = match pwd {
Some(p) if !p.is_empty() => Path::new(p),
_ => {
log::trace!("detect_from: PWD is unset or empty, no mapping");
return LogicalPathContext { mapping: None };
}
};
if pwd == canonical_cwd {
log::trace!("detect_from: PWD == canonical CWD, no mapping");
return LogicalPathContext { mapping: None };
}
match std::fs::canonicalize(pwd) {
Ok(canonical_pwd) if canonical_pwd == canonical_cwd => {}
_ => {
log::trace!("detect_from: PWD validation failed (stale or divergent), no mapping");
return LogicalPathContext { mapping: None };
}
}
match find_divergence_point(canonical_cwd, pwd) {
Some((canonical_prefix, logical_prefix)) => {
log::debug!(
"detect_from: mapping detected: {} → {}",
canonical_prefix.display(),
logical_prefix.display()
);
LogicalPathContext {
mapping: Some(PrefixMapping {
canonical_prefix,
logical_prefix,
}),
}
}
None => {
log::trace!("detect_from: no divergence found");
LogicalPathContext { mapping: None }
}
}
}
#[cfg(windows)]
pub(crate) fn detect_from_cwd(cwd: &Path, canonical_cwd: &Path) -> LogicalPathContext {
if cwd == canonical_cwd {
log::trace!("detect_from_cwd: cwd == canonical_cwd, no mapping");
return LogicalPathContext { mapping: None };
}
match find_divergence_point(canonical_cwd, cwd) {
Some((canonical_prefix, logical_prefix)) => {
log::debug!(
"detect_from_cwd: mapping detected: {} → {}",
canonical_prefix.display(),
logical_prefix.display()
);
LogicalPathContext {
mapping: Some(PrefixMapping {
canonical_prefix,
logical_prefix,
}),
}
}
None => {
log::trace!("detect_from_cwd: no divergence found");
LogicalPathContext { mapping: None }
}
}
}
#[must_use]
pub fn has_mapping(&self) -> bool {
self.mapping.is_some()
}
#[must_use]
pub fn to_logical(&self, path: &Path) -> PathBuf {
self.translate(path, TranslationDirection::ToLogical)
}
#[must_use]
pub fn to_canonical(&self, path: &Path) -> PathBuf {
self.translate(path, TranslationDirection::ToCanonical)
}
fn translate(&self, path: &Path, direction: TranslationDirection) -> PathBuf {
let fallback = path.to_path_buf();
let mapping = match &self.mapping {
Some(m) => m,
None => {
log::trace!("translate: no mapping, returning input unchanged");
return fallback;
}
};
if path.is_relative() {
log::trace!("translate: relative path, returning input unchanged");
return fallback;
}
let (from_prefix, to_prefix) = match direction {
TranslationDirection::ToLogical => (&mapping.canonical_prefix, &mapping.logical_prefix),
TranslationDirection::ToCanonical => {
(&mapping.logical_prefix, &mapping.canonical_prefix)
}
};
#[cfg(windows)]
let path_for_match_buf = strip_extended_length_prefix(path);
#[cfg(windows)]
let path_for_match = path_for_match_buf.as_path();
#[cfg(not(windows))]
let path_for_match = path;
let suffix = match path_for_match.strip_prefix(from_prefix) {
Ok(s) => s,
Err(_) => {
log::trace!(
"translate: path does not start with source prefix ({}), returning unchanged",
from_prefix.display()
);
return fallback;
}
};
let translated = to_prefix.join(suffix);
let original_canonical = match std::fs::canonicalize(path) {
Ok(c) => c,
Err(e) => {
log::trace!(
"translate: canonicalize({}) failed: {e}, returning unchanged",
path.display()
);
return fallback;
}
};
let translated_canonical = match std::fs::canonicalize(&translated) {
Ok(c) => c,
Err(e) => {
log::trace!(
"translate: canonicalize({}) failed: {e}, returning unchanged",
translated.display()
);
return fallback;
}
};
#[cfg(windows)]
let original_canonical = strip_extended_length_prefix(&original_canonical);
#[cfg(windows)]
let translated_canonical = strip_extended_length_prefix(&translated_canonical);
if original_canonical == translated_canonical {
translated
} else {
log::trace!(
"translate: round-trip validation failed ({} != {}), returning unchanged",
original_canonical.display(),
translated_canonical.display()
);
fallback
}
}
}
enum TranslationDirection {
ToLogical,
ToCanonical,
}
#[cfg(windows)]
fn strip_extended_length_prefix(path: &Path) -> PathBuf {
let s = match path.to_str() {
Some(s) => s,
None => return path.to_path_buf(),
};
if let Some(rest) = s.strip_prefix(r"\\?\UNC\") {
return PathBuf::from(format!(r"\\{rest}"));
}
if let Some(rest) = s.strip_prefix(r"\\?\") {
let mut chars = rest.chars();
if let Some(drive) = chars.next() {
if drive.is_ascii_alphabetic() {
if let Some(':') = chars.next() {
return PathBuf::from(rest);
}
}
}
}
path.to_path_buf()
}
fn components_equal(a: &std::path::Component<'_>, b: &std::path::Component<'_>) -> bool {
#[cfg(windows)]
{
a.as_os_str().eq_ignore_ascii_case(b.as_os_str())
}
#[cfg(not(windows))]
{
a == b
}
}
fn find_divergence_point(canonical: &Path, logical: &Path) -> Option<(PathBuf, PathBuf)> {
let canonical_components: Vec<_> = canonical.components().collect();
let logical_components: Vec<_> = logical.components().collect();
let mut common_suffix_len = 0;
let mut c_iter = canonical_components.iter().rev();
let mut l_iter = logical_components.iter().rev();
loop {
match (c_iter.next(), l_iter.next()) {
(Some(c), Some(l)) if components_equal(c, l) => common_suffix_len += 1,
_ => break,
}
}
if common_suffix_len == 0 {
if canonical == logical {
return None;
}
return Some((canonical.to_path_buf(), logical.to_path_buf()));
}
let canonical_prefix_len = canonical_components.len() - common_suffix_len;
let logical_prefix_len = logical_components.len() - common_suffix_len;
if canonical_prefix_len == 0 && logical_prefix_len == 0 {
return None;
}
let canonical_prefix: PathBuf = canonical_components[..canonical_prefix_len]
.iter()
.collect();
let logical_prefix: PathBuf = logical_components[..logical_prefix_len].iter().collect();
if canonical_prefix == logical_prefix {
return None;
}
Some((canonical_prefix, logical_prefix))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn has_mapping_returns_false_when_none() {
let ctx = LogicalPathContext { mapping: None };
assert!(!ctx.has_mapping());
}
#[test]
fn has_mapping_returns_true_when_some() {
let ctx = LogicalPathContext {
mapping: Some(PrefixMapping {
canonical_prefix: PathBuf::from("/mnt/wsl/workspace"),
logical_prefix: PathBuf::from("/workspace"),
}),
};
assert!(ctx.has_mapping());
}
#[test]
fn logical_path_context_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<LogicalPathContext>();
}
#[test]
fn default_returns_no_mapping() {
let ctx = LogicalPathContext::default();
assert!(!ctx.has_mapping());
assert_eq!(ctx, LogicalPathContext { mapping: None });
}
#[cfg(unix)]
#[test]
fn divergence_identical_paths_returns_none() {
let result = find_divergence_point(
Path::new("/home/user/project"),
Path::new("/home/user/project"),
);
assert_eq!(result, None);
}
#[cfg(unix)]
#[test]
fn divergence_common_suffix_different_prefixes() {
let result = find_divergence_point(
Path::new("/mnt/wsl/workspace/project/src"),
Path::new("/workspace/project/src"),
);
assert_eq!(
result,
Some((PathBuf::from("/mnt/wsl"), PathBuf::from("/")))
);
}
#[cfg(unix)]
#[test]
fn divergence_no_common_components_returns_full_paths() {
let result = find_divergence_point(Path::new("/a/b/c"), Path::new("/x/y/z"));
assert_eq!(
result,
Some((PathBuf::from("/a/b/c"), PathBuf::from("/x/y/z")))
);
}
#[cfg(unix)]
#[test]
fn divergence_trailing_slashes() {
let result = find_divergence_point(
Path::new("/real/base/project/"),
Path::new("/link/project/"),
);
assert_eq!(
result,
Some((PathBuf::from("/real/base"), PathBuf::from("/link")))
);
}
#[cfg(unix)]
#[test]
fn divergence_dot_components() {
let result = find_divergence_point(
Path::new("/real/./base/project"),
Path::new("/link/./project"),
);
assert_eq!(
result,
Some((PathBuf::from("/real/base"), PathBuf::from("/link")))
);
}
#[cfg(unix)]
#[test]
fn divergence_dotdot_components() {
let result = find_divergence_point(
Path::new("/real/base/../base/project"),
Path::new("/link/project"),
);
assert_eq!(
result,
Some((PathBuf::from("/real/base/../base"), PathBuf::from("/link")))
);
}
#[cfg(unix)]
#[test]
fn divergence_redundant_separators() {
let result = find_divergence_point(
Path::new("/real///base//project"),
Path::new("/link//project"),
);
assert_eq!(
result,
Some((PathBuf::from("/real/base"), PathBuf::from("/link")))
);
}
#[cfg(unix)]
#[test]
fn divergence_macos_private_prefix() {
let result = find_divergence_point(
Path::new("/private/var/folders/tmp"),
Path::new("/var/folders/tmp"),
);
assert_eq!(
result,
Some((PathBuf::from("/private"), PathBuf::from("/")))
);
}
#[cfg(not(windows))]
#[test]
fn detect_from_pwd_matches_canonical_returns_no_mapping() {
use std::ffi::OsStr;
let cwd = Path::new("/home/user/project");
let ctx = LogicalPathContext::detect_from(Some(OsStr::new("/home/user/project")), cwd);
assert!(!ctx.has_mapping());
}
#[cfg(not(windows))]
#[test]
fn detect_from_pwd_none_returns_no_mapping() {
let cwd = Path::new("/home/user/project");
let ctx = LogicalPathContext::detect_from(None, cwd);
assert!(!ctx.has_mapping());
}
#[cfg(not(windows))]
#[test]
fn detect_from_stale_pwd_returns_no_mapping() {
use std::ffi::OsStr;
let cwd = Path::new("/home/user/project");
let ctx = LogicalPathContext::detect_from(
Some(OsStr::new("/nonexistent/stale/path/project")),
cwd,
);
assert!(!ctx.has_mapping());
}
#[cfg(not(windows))]
#[test]
fn detect_from_corrupted_pwd_returns_no_mapping() {
use std::ffi::OsStr;
let cwd = Path::new("/home/user/project");
let ctx = LogicalPathContext::detect_from(Some(OsStr::new("")), cwd);
assert!(!ctx.has_mapping());
}
#[cfg(target_os = "macos")]
#[test]
fn detect_from_macos_private_prefix_has_mapping() {
use std::ffi::OsStr;
let logical_path = Path::new("/var/folders");
let Ok(canonical_cwd) = std::fs::canonicalize(logical_path) else {
return; };
if canonical_cwd == logical_path {
return; }
let ctx = LogicalPathContext::detect_from(Some(OsStr::new("/var/folders")), &canonical_cwd);
assert!(ctx.has_mapping());
}
fn ctx_with_mapping(
canonical: impl AsRef<Path>,
logical: impl AsRef<Path>,
) -> LogicalPathContext {
LogicalPathContext {
mapping: Some(PrefixMapping {
canonical_prefix: canonical.as_ref().to_path_buf(),
logical_prefix: logical.as_ref().to_path_buf(),
}),
}
}
fn ctx_no_mapping() -> LogicalPathContext {
LogicalPathContext { mapping: None }
}
#[cfg(unix)]
#[test]
fn to_logical_translates_path_under_canonical_prefix() {
let dir = tempfile::tempdir().unwrap();
let canonical_base = dir.path().join("real");
let logical_base = dir.path().join("link");
std::fs::create_dir_all(canonical_base.join("src")).unwrap();
#[cfg(unix)]
std::os::unix::fs::symlink(&canonical_base, &logical_base).unwrap();
let ctx = ctx_with_mapping(&canonical_base, &logical_base);
let input = canonical_base.join("src");
let result = ctx.to_logical(&input);
assert_eq!(result, logical_base.join("src"));
}
#[test]
fn to_logical_returns_input_when_not_under_prefix() {
let ctx = ctx_with_mapping("/mnt/wsl/workspace", "/workspace");
let input = Path::new("/some/other/path");
let result = ctx.to_logical(input);
assert_eq!(result, input.to_path_buf());
}
#[test]
fn to_logical_returns_input_when_no_mapping() {
let ctx = ctx_no_mapping();
let input = Path::new("/home/user/project/src/main.rs");
let result = ctx.to_logical(input);
assert_eq!(result, input.to_path_buf());
}
#[test]
fn to_logical_returns_input_for_relative_path() {
let ctx = ctx_with_mapping("/mnt/wsl/workspace", "/workspace");
let input = Path::new("src/main.rs");
let result = ctx.to_logical(input);
assert_eq!(result, input.to_path_buf());
}
#[cfg(unix)]
#[test]
fn to_canonical_translates_path_under_logical_prefix() {
let dir = tempfile::tempdir().unwrap();
let canonical_base = dir.path().join("real");
let logical_base = dir.path().join("link");
std::fs::create_dir_all(canonical_base.join("src")).unwrap();
#[cfg(unix)]
std::os::unix::fs::symlink(&canonical_base, &logical_base).unwrap();
let ctx = ctx_with_mapping(&canonical_base, &logical_base);
let input = logical_base.join("src");
let result = ctx.to_canonical(&input);
assert_eq!(result, canonical_base.join("src"));
}
#[test]
fn to_canonical_returns_input_when_not_under_prefix() {
let ctx = ctx_with_mapping("/mnt/wsl/workspace", "/workspace");
let input = Path::new("/some/other/path");
let result = ctx.to_canonical(input);
assert_eq!(result, input.to_path_buf());
}
#[test]
fn to_canonical_returns_input_when_no_mapping() {
let ctx = ctx_no_mapping();
let input = Path::new("/home/user/project/src/main.rs");
let result = ctx.to_canonical(input);
assert_eq!(result, input.to_path_buf());
}
#[test]
fn to_canonical_returns_input_for_relative_path() {
let ctx = ctx_with_mapping("/mnt/wsl/workspace", "/workspace");
let input = Path::new("../foo/bar.rs");
let result = ctx.to_canonical(input);
assert_eq!(result, input.to_path_buf());
}
#[cfg(unix)]
#[test]
fn to_logical_falls_back_when_roundtrip_fails() {
let dir = tempfile::tempdir().unwrap();
let real_base = dir.path().join("real");
let bogus_logical = dir.path().join("bogus_link");
std::fs::create_dir_all(real_base.join("src")).unwrap();
let ctx = ctx_with_mapping(&real_base, &bogus_logical);
let input = real_base.join("src");
let result = ctx.to_logical(&input);
assert_eq!(result, input);
}
#[cfg(unix)]
#[test]
fn to_canonical_falls_back_when_roundtrip_fails() {
let dir = tempfile::tempdir().unwrap();
let bogus_canonical = dir.path().join("bogus_real");
let link_base = dir.path().join("link");
std::fs::create_dir_all(link_base.join("src")).unwrap();
let ctx = ctx_with_mapping(&bogus_canonical, &link_base);
let input = link_base.join("src");
let result = ctx.to_canonical(&input);
assert_eq!(result, input);
}
#[cfg(unix)]
#[test]
fn non_utf8_paths_dont_panic() {
use std::ffi::OsStr;
use std::os::unix::ffi::OsStrExt;
let non_utf8 = OsStr::from_bytes(&[0xff, 0xfe]);
let ctx = ctx_with_mapping("/mnt/wsl/workspace", "/workspace");
let input = Path::new(non_utf8);
let result = ctx.to_logical(input);
assert_eq!(result, input.to_path_buf());
let result = ctx.to_canonical(input);
assert_eq!(result, input.to_path_buf());
let ctx2 = LogicalPathContext::detect_from(Some(non_utf8), Path::new("/home/user"));
let _ = ctx2;
}
#[cfg(unix)]
#[test]
fn to_logical_idempotent_on_logical_path() {
use std::os::unix::fs::symlink;
let dir = tempfile::tempdir().unwrap();
let canonical_base = dir.path().join("real");
let logical_base = dir.path().join("link");
std::fs::create_dir_all(canonical_base.join("src")).unwrap();
symlink(&canonical_base, &logical_base).unwrap();
let ctx = ctx_with_mapping(&canonical_base, &logical_base);
let logical_path = logical_base.join("src");
let result = ctx.to_logical(&logical_path);
assert_eq!(result, logical_path);
}
#[cfg(unix)]
#[test]
fn to_canonical_idempotent_on_canonical_path() {
use std::os::unix::fs::symlink;
let dir = tempfile::tempdir().unwrap();
let canonical_base = dir.path().join("real");
let logical_base = dir.path().join("link");
std::fs::create_dir_all(canonical_base.join("src")).unwrap();
symlink(&canonical_base, &logical_base).unwrap();
let ctx = ctx_with_mapping(&canonical_base, &logical_base);
let canonical_path = canonical_base.join("src");
let result = ctx.to_canonical(&canonical_path);
assert_eq!(result, canonical_path);
}
#[cfg(not(windows))]
#[test]
fn detect_from_divergent_pwd_returns_no_mapping() {
let dir = tempfile::tempdir().unwrap();
let dir_a = dir.path().join("a");
let dir_b = dir.path().join("b");
std::fs::create_dir_all(&dir_a).unwrap();
std::fs::create_dir_all(&dir_b).unwrap();
let canonical_a = std::fs::canonicalize(&dir_a).unwrap();
let canonical_b = std::fs::canonicalize(&dir_b).unwrap();
let ctx = LogicalPathContext::detect_from(Some(canonical_a.as_os_str()), &canonical_b);
assert!(!ctx.has_mapping());
}
#[cfg(unix)]
#[test]
fn to_logical_translates_file_paths() {
use std::os::unix::fs::symlink;
let dir = tempfile::tempdir().unwrap();
let canonical_base = dir.path().join("real");
let logical_base = dir.path().join("link");
std::fs::create_dir_all(canonical_base.join("src")).unwrap();
std::fs::write(canonical_base.join("src").join("main.rs"), b"fn main() {}").unwrap();
symlink(&canonical_base, &logical_base).unwrap();
let ctx = ctx_with_mapping(&canonical_base, &logical_base);
let canonical_file = canonical_base.join("src").join("main.rs");
let result = ctx.to_logical(&canonical_file);
assert_eq!(result, logical_base.join("src").join("main.rs"));
let logical_file = logical_base.join("src").join("main.rs");
let back = ctx.to_canonical(&logical_file);
assert_eq!(back, canonical_base.join("src").join("main.rs"));
}
#[cfg(unix)]
#[test]
fn roundtrip_parameterized_test() {
use std::os::unix::fs::symlink;
let dir = tempfile::tempdir().unwrap();
let base = std::fs::canonicalize(dir.path()).unwrap();
let real_base = base.join("real");
let link_base = base.join("link");
let subdirs = [
"src",
"src/main",
"src/lib",
"tests",
"tests/unit",
"docs",
"docs/api",
"build",
"build/debug",
"config",
];
for subdir in &subdirs {
std::fs::create_dir_all(real_base.join(subdir)).unwrap();
}
symlink(&real_base, &link_base).unwrap();
let ctx = LogicalPathContext::detect_from(
Some(link_base.join("src").as_os_str()),
&real_base.join("src"),
);
for subdir in &subdirs {
let canonical = real_base.join(subdir);
let logical = ctx.to_logical(&canonical);
let expected_logical = link_base.join(subdir);
assert_eq!(
logical, expected_logical,
"to_logical failed for {}",
subdir
);
let back_to_canonical = ctx.to_canonical(&logical);
assert_eq!(
back_to_canonical, canonical,
"to_canonical round-trip failed for {}",
subdir
);
}
}
#[cfg(windows)]
#[test]
fn strip_prefix_drive_letter() {
let result = strip_extended_length_prefix(Path::new(r"\\?\C:\Users\dev"));
assert_eq!(result, PathBuf::from(r"C:\Users\dev"));
}
#[cfg(windows)]
#[test]
fn strip_prefix_unc() {
let result = strip_extended_length_prefix(Path::new(r"\\?\UNC\server\share\folder"));
assert_eq!(result, PathBuf::from(r"\\server\share\folder"));
}
#[cfg(windows)]
#[test]
fn strip_prefix_no_prefix_unchanged() {
let result = strip_extended_length_prefix(Path::new(r"C:\Users\dev"));
assert_eq!(result, PathBuf::from(r"C:\Users\dev"));
}
#[cfg(windows)]
#[test]
fn strip_prefix_empty_unchanged() {
let result = strip_extended_length_prefix(Path::new(""));
assert_eq!(result, PathBuf::from(""));
}
#[cfg(windows)]
#[test]
fn divergence_case_insensitive_matching_components() {
let result = find_divergence_point(
Path::new(r"C:\Users\Dev\Project"),
Path::new(r"C:\users\dev\project"),
);
assert_eq!(result, None);
}
#[cfg(windows)]
#[test]
fn divergence_windows_junction_like_paths() {
let result = find_divergence_point(
Path::new(r"D:\Projects\Workspace\src"),
Path::new(r"C:\workspace\src"),
);
assert_eq!(
result,
Some((PathBuf::from(r"D:\Projects"), PathBuf::from(r"C:\")))
);
}
#[cfg(windows)]
#[test]
fn divergence_windows_identical_paths() {
let result = find_divergence_point(
Path::new(r"C:\Users\dev\project"),
Path::new(r"C:\Users\dev\project"),
);
assert_eq!(result, None);
}
#[cfg(windows)]
#[test]
fn detect_from_cwd_equal_paths_no_mapping() {
let ctx = LogicalPathContext::detect_from_cwd(
Path::new(r"C:\Users\dev\project"),
Path::new(r"C:\Users\dev\project"),
);
assert!(!ctx.has_mapping());
}
#[cfg(windows)]
#[test]
fn detect_from_cwd_different_paths_with_common_suffix() {
let ctx = LogicalPathContext::detect_from_cwd(
Path::new(r"S:\workspace\src"),
Path::new(r"D:\projects\workspace\src"),
);
assert!(ctx.has_mapping());
}
#[cfg(windows)]
#[test]
fn detect_from_cwd_different_paths_no_common_suffix() {
let ctx = LogicalPathContext::detect_from_cwd(
Path::new(r"X:\completely\different"),
Path::new(r"Y:\totally\unrelated"),
);
assert!(ctx.has_mapping());
}
#[cfg(windows)]
#[test]
fn to_logical_strips_extended_prefix_from_input() {
let dir = tempfile::tempdir().unwrap();
let canonical_base = std::fs::canonicalize(dir.path()).unwrap();
let real_dir = canonical_base.join("real");
let link_dir = canonical_base.join("link");
let real_dir_stripped = strip_extended_length_prefix(&real_dir);
let link_dir_stripped = strip_extended_length_prefix(&link_dir);
std::fs::create_dir_all(real_dir.join("src")).unwrap();
let status = std::process::Command::new("cmd")
.args(["/C", "mklink", "/J"])
.arg(&link_dir)
.arg(&real_dir)
.output()
.expect("mklink /J");
assert!(status.status.success(), "mklink /J failed");
let ctx = ctx_with_mapping(&real_dir_stripped, &link_dir_stripped);
let input = real_dir.join("src");
let result = ctx.to_logical(&input);
assert_eq!(result, link_dir_stripped.join("src"));
let _ = std::process::Command::new("cmd")
.args(["/C", "rd"])
.arg(&link_dir)
.output();
}
#[cfg(windows)]
#[test]
fn detect_from_cwd_identical_returns_fallback() {
let path = Path::new(r"C:\Users\dev\project");
let ctx = LogicalPathContext::detect_from_cwd(path, path);
assert!(!ctx.has_mapping());
let input = Path::new(r"C:\Users\dev\project\src\main.rs");
assert_eq!(ctx.to_logical(input), input.to_path_buf());
assert_eq!(ctx.to_canonical(input), input.to_path_buf());
}
#[cfg(windows)]
#[test]
fn windows_relative_path_returns_unchanged() {
let ctx = ctx_with_mapping(r"D:\projects\workspace", r"C:\workspace");
let input = Path::new(r"src\main.rs");
assert_eq!(ctx.to_logical(input), input.to_path_buf());
assert_eq!(ctx.to_canonical(input), input.to_path_buf());
}
#[cfg(windows)]
#[test]
fn divergence_non_ascii_case_is_not_folded() {
let result = find_divergence_point(
Path::new(r"C:\Users\Ä\project"),
Path::new(r"C:\Users\ä\project"),
);
assert_eq!(
result,
Some((PathBuf::from(r"C:\Users\Ä"), PathBuf::from(r"C:\Users\ä"),))
);
}
#[cfg(windows)]
#[test]
fn strip_prefix_volume_guid_unchanged() {
let input = r"\\?\Volume{12345678-1234-1234-1234-123456789abc}\Users\dev";
let result = strip_extended_length_prefix(Path::new(input));
assert_eq!(result, PathBuf::from(input));
}
}