logical-path 0.1.0

Translate canonical (symlink-resolved) filesystem paths back to their logical (symlink-preserving) equivalents
Documentation
  • Coverage
  • 100%
    6 out of 6 items documented5 out of 6 items with examples
  • Size
  • Source code size: 153.45 kB This is the summed size of all the files inside the crates.io package for this release.
  • Documentation size: 446.43 kB This is the summed size of all files generated by rustdoc for all configured targets
  • Ø build duration
  • this release: 7s Average build duration of successful builds.
  • all releases: 7s Average build duration of successful builds in releases after 2024-10-23.
  • Links
  • brooke-hamilton/logical-path
    0 0 0
  • crates.io
  • Dependencies
  • Versions
  • Owners
  • brooke-hamilton

logical-path

Crates.io Docs.rs License: MIT CI

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?

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:

[dependencies]
logical-path = "0.1"

Quick Start

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

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

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 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/ 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.