Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions .github/agents/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
````markdown
# RCDir Development Guidelines

Auto-generated from all feature plans. Last updated: 2026-04-11
Auto-generated from all feature plans. Last updated: 2026-04-19

## Active Technologies
- Rust stable (edition 2024, toolchain 1.85+) + `windows` crate 0.62 (Win32 API), `widestring` 1 (UTF-16) (003-file-icons)
Expand All @@ -10,6 +10,8 @@ Auto-generated from all feature plans. Last updated: 2026-04-11
- N/A (filesystem enumeration, no persistent storage) (004-tree-view)
- Rust stable (latest stable release) + `windows` crate for Win32 console API; standard library for file I/O (`std::fs::read`) (006-config-file-support)
- Single flat file (`%USERPROFILE%\.rcdirconfig`), UTF-8 with optional BOM (006-config-file-support)
- Rust stable (latest stable release, per rust-toolchain.toml) + `windows` crate (Win32 API: `CreateFileW`, `DeviceIoControl`, `FSCTL_GET_REPARSE_POINT`) (007-symlink-junction-targets)
- N/A (filesystem reads only, no persistent state) (007-symlink-junction-targets)

- Rust stable (1.93.0), Edition 2024 + `windows` crate (Win32 APIs), `widestring` (UTF-16 interop) (master)

Expand All @@ -29,9 +31,9 @@ cargo test; cargo clippy
Rust stable (1.93.0), Edition 2024: Follow standard conventions

## Recent Changes
- 007-symlink-junction-targets: Added Rust stable (latest stable release, per rust-toolchain.toml) + `windows` crate (Win32 API: `CreateFileW`, `DeviceIoControl`, `FSCTL_GET_REPARSE_POINT`)
- 006-config-file-support: Added Rust stable (latest stable release) + `windows` crate for Win32 console API; standard library for file I/O (`std::fs::read`)
- 004-tree-view: Added Rust stable (edition 2024) + `windows` crate (Win32 API), `widestring` crate, Rust std library only (no third-party libraries)
- 004-tree-view: Added Rust stable (edition 2024) + `windows` crate (Win32 API), `widestring` crate, Rust std library only (no third-party libraries)
Fix- 004-tree-view: Added Rust stable (edition 2024) + `windows` crate (Win32 API), `widestring` crate, Rust std library only (no third-party libraries)


<!-- MANUAL ADDITIONS START -->
Expand Down
30 changes: 29 additions & 1 deletion .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,13 @@ console.print_error(&format!("Error: {}", msg));
- **No test may run the real `rcdir` binary** — test the library functions directly with mocked dependencies
- Temp files are acceptable **only** in explicitly marked integration tests, never in unit tests

### Output Parity Tests — Required for All Features
- Every user-visible feature or bug fix MUST include output parity tests in `tests/output_parity.rs`
- Parity tests run both `rcdir` and `tcdir` with the same arguments and assert byte-identical output
- These are an **allowed exception** to the unit test isolation rules above — they run real binaries
- Parity tests gracefully skip when `tcdir.exe` is not available (CI environments)
- When adding a new feature, add parity test cases covering all affected display modes (normal, tree, wide, bare as applicable)

---

## Communication Rules
Expand Down Expand Up @@ -222,6 +229,27 @@ console.print_error(&format!("Error: {}", msg));
- `cargo check` — quick compilation verification
- `cargo clippy` — lint checking

### Toolchain Currency
- **ALWAYS** run `rustup update stable` before starting work to ensure the local toolchain matches CI
- CI uses `dtolnay/rust-toolchain@stable` which installs the latest stable Rust on every run
- New stable releases (every 6 weeks) can introduce new clippy lints that break `-D warnings`
- A toolchain mismatch between local and CI is the most common cause of "works locally, fails in CI"

### Pre-Push Checklist
- **ALWAYS** run `cargo clippy -- -D warnings` and verify zero errors before pushing
- **ALWAYS** run `cargo test` and verify all tests pass before pushing
- If clippy introduces new warnings after a toolchain update, fix them before pushing

### Pre-Commit Gates
- **ALL** tests MUST pass before committing (`cargo test`)
- Clippy MUST be clean (`cargo clippy -- -D warnings`) before committing
- Build MUST succeed with no errors before committing

### Commit Frequency
- During spec implementation, commit **at least once per completed phase**
- Each commit must leave the codebase in a compilable, tests-passing, clippy-clean state
- Do not batch an entire feature into a single commit

### Build Integration
- Always build after making changes using the build task or `Build.ps1`
- Fix all clippy warnings before considering task complete
Expand Down Expand Up @@ -255,7 +283,7 @@ console.print_error(&format!("Error: {}", msg));

---

*Last Updated: 2026-02-16*
*Last Updated: 2026-04-20*
*These rules apply globally to all RCDir work*

````
5 changes: 3 additions & 2 deletions .specify/memory/constitution.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ All user-facing output MUST follow established patterns:
- **Error Messages**: Errors go to stderr; user-facing messages MUST be clear, actionable, and consistent in tone
- **Help System**: All features MUST be documented in `-?` help output and `--env`/`--config` where applicable
- **Backward Compatibility**: Maintain compatibility with TCDir's command-line interface
- **Output Parity (NON-NEGOTIABLE)**: Every feature and bug fix MUST produce byte-identical visible output to TCDir for the same inputs. Output parity tests in `tests/output_parity.rs` MUST be added or updated for every user-visible change. These parity tests are an allowed exception to the test isolation rule — they run real binaries and compare output.

**Rationale**: RCDir is a Rust port of TCDir; users expect consistent behavior with the original.
**Rationale**: RCDir is a Rust port of TCDir; users expect identical behavior with the original. Output parity tests are the final verification that the port is correct.

### IV. Performance Requirements

Expand Down Expand Up @@ -149,6 +150,6 @@ This constitution supersedes all ad-hoc practices. All code changes MUST verify

**Guidance Reference**: See `.github/copilot-instructions.md` for detailed runtime development guidance and code style rules.

**Version**: 1.2.0 | **Ratified**: 2026-02-07 | **Last Amended**: 2026-04-20
**Version**: 1.3.0 | **Ratified**: 2026-02-07 | **Last Amended**: 2026-04-20

```
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@

All notable changes to RCDir are documented in this file.

## [5.4] - 2026-04-20

### Added

- Symlink, junction, and AppExecLink target display: `filename → target` in normal and tree modes
- Junctions show resolved target path with `\??\` device prefix stripped
- File and directory symlinks show target path as-stored (relative or absolute)
- AppExecLink entries (e.g., `python.exe` in WindowsApps) show resolved executable path
- Arrow (`→`) uses Information color; target uses the filename's own color
- Wide and bare modes unaffected (no target display)
- Graceful degradation: inaccessible reparse data shows filename without target
- Zero overhead for non-reparse files (single attribute flag check)
- 3 new output parity tests for reparse target display (normal, tree, AppExecLink)
- 469 unit tests (up from 435)

## [5.3.1403] - 2026-04-11

### Added
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ It's designed as a practical `dir`-style command with useful defaults (color by

| Version | Highlights |
|---------|------------|
| **5.4** | Symlink, junction, and AppExecLink target display (`→ target`) |
| **5.3** | Config file support (`.rcdirconfig`), `--config` diagnostics, `--settings` merged view |
| **5.2** | Interactive PowerShell alias configuration (`--set-aliases`, `--get-aliases`, `--remove-aliases`) |
| **5.1** | `--Tree` hierarchical directory view with depth control |
Expand All @@ -37,6 +38,7 @@ Hat tip to [Chris Kirmse](https://github.com/ckirmse) whose excellent [ZDir](htt
| Multi-threaded enumeration | — | ✅ | — | — |
| Native Windows (no WSL/MSYS) | ✅ | ✅ | ⚠️ | ⚠️ |
| Familiar `dir` switch syntax | ✅ | ✅ | — | — |
| Symlink/junction target display | — | ✅ | ✅ | ✅ |
| ARM64 native binary | ✅ | ✅ | — | — |
| NTFS alternate data streams | ✅ | ✅ | — | — |
| Configurable via environment variable | — | ✅ | — | — |
Expand Down
4 changes: 2 additions & 2 deletions Version.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# RCDir version — build number auto-incremented by build.rs on every compile.
# Major and minor are updated manually.
major = 5
minor = 3
build = 1403
minor = 4
build = 1404
108 changes: 108 additions & 0 deletions specs/007-symlink-junction-targets/data-model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Data Model: Symlink & Junction Target Display

## Entities

### ReparseDataBuffer (manual struct definition)

Mirrors the Windows `REPARSE_DATA_BUFFER` layout. Not available in the `windows` crate; defined manually.

```rust
#[repr(C)]
struct ReparseDataBufferHeader {
reparse_tag: u32, // IO_REPARSE_TAG_MOUNT_POINT, SYMLINK, or APPEXECLINK
reparse_data_length: u16, // Length of data after this header
reserved: u16, // Must be 0
}
```

**Junction / Mount Point buffer** (after header):
```rust
#[repr(C)]
struct MountPointReparseBuffer {
substitute_name_offset: u16,
substitute_name_length: u16,
print_name_offset: u16,
print_name_length: u16,
// Followed by PathBuffer: [u16; N] containing both strings
}
```

**Symlink buffer** (after header):
```rust
#[repr(C)]
struct SymbolicLinkReparseBuffer {
substitute_name_offset: u16,
substitute_name_length: u16,
print_name_offset: u16,
print_name_length: u16,
flags: u32, // SYMLINK_FLAG_RELATIVE = 0x00000001
// Followed by PathBuffer: [u16; N] containing both strings
}
```

**AppExecLink buffer** (after header):
```
[u32 version] // Must be 3
[NUL-terminated UTF-16] // Package family name
[NUL-terminated UTF-16] // App user model ID
[NUL-terminated UTF-16] // Target executable path ← displayed
```

### FileInfo (existing, modified)

| Field | Type | Change | Description |
|-------|------|--------|-------------|
| `reparse_tag` | `u32` | Existing | Already populated from `WIN32_FIND_DATA.dwReserved0` |
| `reparse_target` | `String` | **NEW** | Resolved target path; empty if not a supported reparse point or resolution failed |

### Supported Reparse Tags

| Constant | Value | Type | Target Source |
|----------|-------|------|---------------|
| `IO_REPARSE_TAG_MOUNT_POINT` | `0xA000_0003` | Junction | PrintName (preferred) or SubstituteName with `\??\` stripped |
| `IO_REPARSE_TAG_SYMLINK` | `0xA000_000C` | Symlink | PrintName (preferred) or SubstituteName (strip `\??\` for absolute only) |
| `IO_REPARSE_TAG_APPEXECLINK` | `0x8000_001B` | App alias | Third NUL-terminated string in version-3 buffer |

### Display Format

```
{filename} → {target_path}
^ ^
| └─ filename's own color attribute
└─ Information color attribute (U+2192 RIGHTWARDS ARROW)
```

- Exactly one space before and after the arrow
- Arrow character: `→` (U+2192)
- Only displayed in normal mode and tree mode
- NOT displayed in wide mode or bare mode

## Relationships

```
FileInfo (1) ──has──> (0..1) reparse_target: String
├── reparse_tag determines which parser to invoke
├── FILE_ATTRIBUTE_REPARSE_POINT flag gates resolution
└── Empty string = not resolved / not applicable / error

ReparseResolver
├── resolve_reparse_target() ──calls──> CreateFileW + DeviceIoControl
├── parse_junction_buffer() ──reads──> MountPointReparseBuffer
├── parse_symlink_buffer() ──reads──> SymbolicLinkReparseBuffer
├── parse_app_exec_link_buffer() ──reads──> GenericReparseBuffer
└── strip_device_prefix() ──strips──> "\??\" prefix
```

## Validation Rules

- `reparse_tag` must match one of the three supported tags before attempting resolution
- `FILE_ATTRIBUTE_REPARSE_POINT` attribute must be set (early exit otherwise)
- Buffer size must be ≥ header size (8 bytes) before parsing
- PrintName/SubstituteName offsets + lengths must not exceed buffer bounds
- AppExecLink version must be 3; reject other versions silently
- NUL terminators must be present within remaining buffer for AppExecLink strings

## State Transitions

N/A — no state machine. Resolution is a one-shot operation during enumeration.
73 changes: 73 additions & 0 deletions specs/007-symlink-junction-targets/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Implementation Plan: Symlink & Junction Target Display

**Branch**: `007-symlink-junction-targets` | **Date**: 2026-04-19 | **Spec**: [spec.md](spec.md)
**Input**: Feature specification from `/specs/007-symlink-junction-targets/spec.md`

## Summary

Display `→ target_path` after symlinks, junctions, and AppExecLinks in normal and tree mode listings. Requires a new reparse point resolver module that reads reparse data via `DeviceIoControl` + `FSCTL_GET_REPARSE_POINT`, parses the three supported buffer formats (junction, symlink, AppExecLink), and stores the resolved target in `FileInfo`. Display integration adds the arrow (Information color) and target path (filename color) after the entry name. Ported from TCDir spec 007.

## Technical Context

**Language/Version**: Rust stable (latest stable release, per rust-toolchain.toml)
**Primary Dependencies**: `windows` crate (Win32 API: `CreateFileW`, `DeviceIoControl`, `FSCTL_GET_REPARSE_POINT`)
**Storage**: N/A (filesystem reads only, no persistent state)
**Testing**: `cargo test` — pure-function buffer parsing tests with synthetic byte arrays
**Target Platform**: Windows 10/11, x64 and ARM64
**Project Type**: CLI application (directory lister)
**Performance Goals**: Zero overhead for non-reparse files (single attribute flag check); <100µs per reparse point resolution
**Constraints**: Stack-allocated 16KB buffer for reparse data; no heap allocation in hot path
**Scale/Scope**: Typically 0–5 reparse points per directory listing; negligible impact on overall listing time

## Constitution Check

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

| Principle | Status | Notes |
|-----------|--------|-------|
| I. Code Quality | ✅ PASS | `Result<T, E>` for all Win32 calls; no `unwrap()` in production; borrowing preferred; idiomatic Rust patterns |
| II. Testing Discipline | ✅ PASS | Pure-function buffer parsers tested with synthetic byte arrays; no file system dependency in unit tests |
| III. User Experience Consistency | ✅ PASS | Arrow uses Information color (matches TCDir); target uses filename color; output parity with TCDir verified |
| IV. Performance Requirements | ✅ PASS | Single flag check for non-reparse files; stack-allocated 16KB buffer; no heap allocation in resolver |
| V. Simplicity & Maintainability | ✅ PASS | Single new module (reparse_resolver); pure parsing functions separated from I/O; minimal display changes |

## Project Structure

### Documentation (this feature)

```text
specs/[###-feature]/
├── plan.md # This file (/speckit.plan command output)
├── research.md # Phase 0 output (/speckit.plan command)
├── data-model.md # Phase 1 output (/speckit.plan command)
├── quickstart.md # Phase 1 output (/speckit.plan command)
├── contracts/ # Phase 1 output (/speckit.plan command)
└── tasks.md # Phase 2 output (/speckit.tasks command - NOT created by /speckit.plan)
```

### Source Code (repository root)

```text
src/
├── file_info.rs # Add reparse_target: String field
├── reparse_resolver.rs # NEW: Win32 reparse data reading + buffer parsing
├── directory_lister.rs # Call resolve_reparse_target() in add_match_to_list()
├── multi_threaded_lister.rs # Same integration as directory_lister
└── results_displayer/
├── normal.rs # Append → target after filename
└── tree.rs # Append → target after filename

tests/
└── (inline #[cfg(test)]) # Pure-function buffer parsing tests in reparse_resolver.rs
```

**Structure Decision**: Single new module `reparse_resolver.rs` at src/ root level. Buffer parsing functions are pure (no I/O), testable inline. Display changes are minimal additions to existing normal and tree displayers.

## Complexity Tracking

> **Fill ONLY if Constitution Check has violations that must be justified**

| Violation | Why Needed | Simpler Alternative Rejected Because |
|-----------|------------|-------------------------------------|
| [e.g., 4th project] | [current need] | [why 3 projects insufficient] |
| [e.g., Repository pattern] | [specific problem] | [why direct DB access insufficient] |
Loading
Loading