# logical-path
[](https://crates.io/crates/logical-path)
[](https://docs.rs/logical-path)
[](LICENSE)
[](https://github.com/brooke-hamilton/logical-path/actions/workflows/ci.yml)
**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()`** calls `getcwd(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?
| [`dunce`](https://crates.io/crates/dunce) | Strips `\\?\` from Windows canonical paths | Doesn't preserve symlinks |
| [`path-absolutize`](https://crates.io/crates/path-absolutize) | Makes paths absolute without resolving symlinks | Lexical only; no `$PWD` awareness |
| [`path-dedot`](https://crates.io/crates/path-dedot) | Removes `.`/`..` lexically | Pure string manipulation |
| [`normalize-path`](https://crates.io/crates/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`:
```toml
[dependencies]
logical-path = "0.1"
```
### Quick Start
```rust
use logical_path::LogicalPathContext;
// Detect any active prefix mapping from the process environment.
// Unix: compares $PWD vs getcwd(). Windows: compares current_dir() vs canonicalize().
let ctx = LogicalPathContext::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(&canonical_path);
// Translate a logical path to its canonical equivalent for filesystem operations.
let fs_path = ctx.to_canonical(&logical_path);
```
### Example: Shell Integration
```rust
use logical_path::LogicalPathContext;
use std::path::Path;
fn emit_cd_directive(target: &Path) {
let ctx = LogicalPathContext::detect();
// Without this, the user would be teleported to the canonical path.
let logical = ctx.to_logical(target);
println!("cd {}", logical.display());
}
```
## Algorithm
`LogicalPathContext::detect()` implements a five-step algorithm:
1. **Detect** — Compare the logical CWD against the canonical CWD. On Unix, this is `$PWD` vs `getcwd()`. On Windows, this is `current_dir()` vs `canonicalize()` (with `\\?\` prefix stripped).
2. **Map** — Suffix-match path components to find the divergence point; extract canonical and logical prefixes.
3. **Translate** — For any canonical path, strip the canonical prefix and prepend the logical prefix.
4. **Validate** — Round-trip check (`canonicalize(translated) == canonicalize(original)`) to catch prefix mappings broad enough to mistranslate unrelated paths.
5. **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
| 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 `cd` directives for shell integration
- Displays filesystem paths to users
- Compares paths from different sources (e.g., `git worktree list` output 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/`](docs/) directory:
- **[Architecture](docs/architecture.md)** — Data model, design invariants, module layout, and testability seams.
- **[How It Works](docs/how-it-works.md)** — Step-by-step walkthrough of the detection and translation algorithm with visual examples.
- **[API Reference](docs/api-reference.md)** — Detailed guide to every public method, with usage patterns and code examples.
- **[Platform Behavior](docs/platform-behavior.md)** — How the crate behaves on Linux, macOS, and Windows, including known quirks and limitations.
- **[Examples](docs/examples.md)** — Real-world integration patterns: shell directives, git worktrees, global context, and more.
- **[FAQ](docs/FAQ.md)** — Frequently asked questions about edge cases, design decisions, and platform support.
API documentation is also available on [docs.rs](https://docs.rs/logical-path).
## 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](LICENSE).