logical-path
Translate canonical (symlink-resolved) filesystem paths back to their logical (symlink-preserving) equivalents.
The Problem
Rust CLI tools that display filesystem paths or emit cd directives silently resolve symlinks, moving users out of their logical directory tree. The root cause is that Rust's standard library provides no way to work with logical paths — only physical ones:
std::env::current_dir()callsgetcwd(2)on Unix, which returns the physical path.std::fs::canonicalize()resolves all symlinks by design.
On Unix, the user's logical path lives in $PWD, a shell convention that std intentionally ignores. On Windows, current_dir() preserves junctions and subst drives but canonicalize() resolves them — and prepends a \\?\ prefix. Any tool that calls these APIs and then shows a path to the user — or writes a cd directive for shell integration — will silently teleport the user out of their logical directory tree.
Why Not an Existing Crate?
| Crate | What it does | Gap |
|---|---|---|
dunce |
Strips \\?\ from Windows canonical paths |
Doesn't preserve symlinks |
path-absolutize |
Makes paths absolute without resolving symlinks | Lexical only; no $PWD awareness |
path-dedot |
Removes ./.. lexically |
Pure string manipulation |
normalize-path |
Normalizes separators and dots | Same as above |
These crates help you avoid canonicalization. The unserved problem is undoing canonicalization after it has already happened — because git, OS APIs, or other tools force it.
Usage
Add the crate to your Cargo.toml:
[]
= "0.1"
Quick Start
use LogicalPathContext;
// Detect any active prefix mapping from the process environment.
// Unix: compares $PWD vs getcwd(). Windows: compares current_dir() vs canonicalize().
let ctx = detect;
// Translate a canonical path to its logical (symlink-preserving) equivalent.
// Falls back to the input path unchanged if no mapping applies.
let display_path = ctx.to_logical;
// Translate a logical path to its canonical equivalent for filesystem operations.
let fs_path = ctx.to_canonical;
Example: Shell Integration
use LogicalPathContext;
use Path;
Algorithm
LogicalPathContext::detect() implements a five-step algorithm:
- Detect — Compare the logical CWD against the canonical CWD. On Unix, this is
$PWDvsgetcwd(). On Windows, this iscurrent_dir()vscanonicalize()(with\\?\prefix stripped). - Map — Suffix-match path components to find the divergence point; extract canonical and logical prefixes.
- Translate — For any canonical path, strip the canonical prefix and prepend the logical prefix.
- Validate — Round-trip check (
canonicalize(translated) == canonicalize(original)) to catch prefix mappings broad enough to mistranslate unrelated paths. - Fall back — Return the canonical path unchanged if detection fails or the mapping doesn't apply.
Note: The Validate step calls
std::fs::canonicalize(), which requires the path to exist on disk. Translation of paths to non-existent files will always fall back to returning the input unchanged.
Platform Notes
| Linux | macOS | Windows | |
|---|---|---|---|
| Logical path source | $PWD |
$PWD |
current_dir() (preserves indirections) |
| Canonical path source | getcwd() |
getcwd() |
canonicalize() with \\?\ stripped |
| System symlinks | User-created only | /var→/private/var, /tmp→/private/tmp |
NTFS junctions, directory symlinks |
| Other indirections | — | — | subst drives, mapped network drives |
| Case sensitivity | Yes | No (APFS default) | No (ordinal case-insensitive) |
canonicalize() quirks |
None | /private prefixing |
\\?\ Extended Length Path prefix |
macOS note: System-level symlinks like /var → /private/var trigger this bug even without any user-created symlinks.
Windows note: Detection works for NTFS junctions, directory symlinks, subst drive letters, and mapped network drives via the current_dir() vs canonicalize() comparison. The \\?\ Extended Length Path prefix returned by canonicalize() is stripped automatically. Path comparison on Windows is ordinal case-insensitive, matching NTFS behavior.
Use Cases
Any Rust CLI tool that:
- Writes
cddirectives for shell integration - Displays filesystem paths to users
- Compares paths from different sources (e.g.,
git worktree listoutput vs the current directory)
Common environments: WSL with mounted VHDs, NFS/network mounts, macOS /var//tmp, custom workspace symlinks, Windows NTFS junctions, subst drives.
Documentation
For more in-depth documentation beyond this README, see the docs/ directory:
- Architecture — Data model, design invariants, module layout, and testability seams.
- How It Works — Step-by-step walkthrough of the detection and translation algorithm with visual examples.
- API Reference — Detailed guide to every public method, with usage patterns and code examples.
- Platform Behavior — How the crate behaves on Linux, macOS, and Windows, including known quirks and limitations.
- Examples — Real-world integration patterns: shell directives, git worktrees, global context, and more.
- FAQ — Frequently asked questions about edge cases, design decisions, and platform support.
API documentation is also available on docs.rs.
Contributing
Contributions are welcome! Please open an issue to discuss any significant changes before submitting a pull request. Bug reports, feature requests, and platform-specific test cases are especially appreciated.
Minimum Supported Rust Version (MSRV)
The minimum supported Rust version is 1.85.0 (required by edition 2024). The MSRV is not changed without a minor-version bump.
License
Licensed under the MIT License.