Skip to content

m4 link state inspection

Kazushi Kamegawa edited this page Jul 27, 2026 · 3 revisions

M4: Link state inspection

Status: Completed. Tracked by issue #6 (closed) and sub-issues #44 through #48 (closed, merged via PRs #96-#100). docs/adr-phase-3.md ADR-0014 through ADR-0017 record the implementation decisions.

Scope

M4 adds read-only inspection of Links\<alias>.exe and reports enough structured state for the later CLI, TUI, and repair milestones. It covers:

  • classification as Ok, Missing, Broken, or Mismatch;
  • decoding Windows symbolic-link reparse data;
  • comparing the resolved target with the expected package executable;
  • deterministic reporting of alias collisions; and
  • unit and filesystem-backed regression coverage.

M4 does not create, delete, or replace links. M5 owns repair and must re-inspect an entry immediately before any mutation.

Classification contract

Observed entry Status Existing target
No entry Missing None
Regular file Mismatch None
Reparse point that is not a symbolic link Mismatch None
Symbolic link whose target is absent Broken Decoded target
Symbolic link that resolves to the expected file Ok Decoded target
Symbolic link that resolves to another existing file Mismatch Decoded target

This makes the M4 checklist and #46 authoritative: Broken is limited to an absent symbolic-link target, while an existing different target is Mismatch. The conflicting sentence in docs/PLAN.md is corrected during #48, and the decision is recorded during #44 in the M4 ADR file.

Unreadable attributes, access denial, sharing violations, malformed reparse data, unexpected I/O failures, and an expected package executable that disappears during inspection are errors, not one of the four normal states. The error records the failed operation, path, and Win32 error code.

Core design

core/Model.h gains:

  • LinkEntryKind { None, RegularFile, SymbolicLink, OtherReparsePoint };
  • the observed entry kind on RepairItem; and
  • AliasCollision, containing an alias and the distinct package executables that resolved to it.

core/LinkInspector.{h,cpp} provides:

  • LinkObservation, containing the entry kind, an optional decoded target, and the target relation NotApplicable, Missing, SameFile, or DifferentFile;
  • classifyLink, a filesystem-independent classifier used directly by deterministic unit tests;
  • inspectLink, the production Win32 adapter returning a RepairItem;
  • a boundary-checked symbolic-link reparse-buffer parser; and
  • detectAliasCollisions, which groups aliases with ordinal case-insensitive comparison and returns stable, sorted collision groups.

The production adapter:

  1. Uses GetFileAttributesW to distinguish absence, a normal file, and a reparse point. Only clean file/path-not-found outcomes mean Missing.
  2. Opens a reparse point with CreateFileW, FILE_FLAG_OPEN_REPARSE_POINT, and read/write/delete sharing, held by a move-only RAII handle.
  3. Reads at most the platform maximum reparse-data size with DeviceIoControl(FSCTL_GET_REPARSE_POINT).
  4. Accepts IO_REPARSE_TAG_SYMLINK as a symbolic link, validates every offset and byte length before reading UTF-16 data, and resolves relative targets against the link's parent. Extended-length (\\?\) and UNC (\\?\UNC\) forms are normalized through the existing paths::fromExtendedLengthPath helper; the NT-namespace (\??\) prefix a symbolic link's SubstituteName uses is not covered by that helper and its decoding is new M4 code, not a reuse of it. Drive-relative forms need no normalization beyond this.
  5. Opens the decoded target and expected executable for FILE_READ_ATTRIBUTES, then compares FILE_ID_INFO volume serials and 128-bit file IDs. It does not silently fall back to lexical path comparison.

Every inspection path is read-only. An unknown reparse tag is preserved as OtherReparsePoint and classified Mismatch, allowing M5 to avoid treating it as a normal symbolic link.

Delivery sequence

  1. #44 - Define link-inspection model and classification contract
    • Add the model, observation, typed error, and pure classification function.
    • Test every classification row and invalid observation combination.
    • Add docs/adr-phase-3.md with the status and re-inspection decisions. (ADR files are numbered sequentially by file, not by milestone: adr.md is file 1, adr-phase-2.md is file 2 and already holds the M3 decisions, so this is file 3 — not "adr-phase-4".)
    • Branch: feature/44-link-inspection-contract.
  2. #45 - Decode symbolic-link reparse targets
    • Add the reparse handle/read path and boundary-checked parser.
    • Test absolute, relative, UNC, and NT-path forms plus malformed buffers and wrong tags.
    • Branch: feature/45-reparse-target-decoder.
  3. #46 - Probe link entries and compare target identity
    • Complete the production inspectLink adapter.
    • Cover normal files, other reparse points, broken targets, matching and differing file identities, and an expected target that disappears.
    • Branch: feature/46-link-target-identity.
  4. #47 - Detect alias collisions deterministically
    • Group aliases without case sensitivity, deduplicate the same executable path, and sort aliases and executable paths deterministically.
    • Report collision groups separately so M6/M7 can warn and exclude them from automatic repair until the user chooses a target.
    • Branch: feature/47-alias-collisions.
  5. #48 - Add regression matrix and finalize documentation
    • Add cross-component regression coverage and update both project and filters files.
    • Correct docs/PLAN.md, complete M4 in docs/TODO.md, and record per-issue evidence in docs/task.md. "Maintain the ADR index" means appending a forward pointer to docs/adr-phase-3.md at the end of docs/adr-phase-2.md, the same way adr.md already points forward to adr-phase-2.md.
    • Branch: feature/48-link-inspection-regression.

#45 is blocked by #44, #46 by #45, #47 by #44, and #48 by both #46 and #47. After #44 merges, #45 and #47 can proceed independently.

Test plan

  • Pure classification tests cover all four statuses, normal files, non-symlink reparse points, invalid observations, and the error boundary.
  • Reparse parser tests cover absolute, relative, UNC, and NT namespace targets; offset and length bounds; odd UTF-16 byte lengths; truncated buffers; and unexpected tags.
  • Filesystem-backed tests cover absent entries, normal files, valid and broken symbolic links, relative links, different targets, and file-identity comparison.
  • Collision tests cover aliases that differ only by case, groups of two and three, distinct aliases, repeated copies of one executable, and stable output after input reordering.
  • Each pull request builds core and tests at warnings-as-errors in Debug|Release times x64|ARM64. Debug and Release x64 tests must run through vstest.console.exe and report the actual result.
  • ARM64 is reported as cross-built, not run, unless the tests execute on an ARM64 host.
  • The executable project's known missing-entry-point linker failure remains expected until M6; M4 completion evidence uses the core and test projects.
  • No third-party dependency is added.

Completion criteria

  • #44 through #48 are merged and closed, and #6 reflects their completion.
  • LinkInspector and collision behavior have deterministic MSTest coverage.
  • All required x64 builds and tests pass; ARM64 builds pass with execution claims stated accurately.
  • docs/PLAN.md, docs/TODO.md, the M4 ADR, docs/task.md, and this Wiki page describe the same behavior.
  • No healthy Ok link is modified, and no M4 code mutates the filesystem.
  • Canonical documentation and comments are English. Localized *_ja.md files are not read or changed.

Clone this wiki locally