GgenIgniter.ArtifactIdentity (ggen_igniter v26.9.8)

Copy Markdown View Source

A real, first-class ARTIFACT IDENTITY primitive: turns a (base_dir, raw_path) pair -- an arbitrary, possibly-relative, possibly-alias-laden path string as it appears in a %GgenIgniter.PendingActuation{}'s target, a GgenIgniter.Manifest entry's outputs key, or a GgenIgniter.Receipt's files entry -- into one CANONICAL identity string, so two lexically different raw strings that resolve to the same real on-disk location are recognized as the SAME identity everywhere this pipeline reasons about "is this the same output."

Why this module exists (a real, confirmed adversarial finding, not a

speculative hardening pass)

.ggen_igniter_factory/redteam-concurrency-nondeterminism.md (read fresh before writing this module, its real reproducer re-run against the fix below) demonstrates a REAL, reproduced defect: :admit's duplicate- output-path guard in GgenIgniter.Reactors.ReconcileReactor used to group pending writes by the raw target STRING (Enum.group_by(& &1.target)) -- never canonicalized. Two targets whose --out/to: strings differ only by a redundant /./ path segment (one concrete, independently filesystem-verified example; //, .., and symlink-based aliases are the same root cause by the same reasoning) resolve to the SAME real inode while comparing as different Elixir strings, silently bypassing the guard and letting :actuate's real Task.async_stream/3 concurrency race unprotected on the shared real file -- genuine, empirically-confirmed last-writer-wins, with the pipeline reporting standing: :alive (full success) regardless of which target's content was actually discarded.

A second real, confirmed defect closed by this module's walk_real_path/3: on a case-insensitive/case-preserving filesystem (macOS default APFS/HFS+), each existing non-symlink path segment's REAL on-disk entry casing is now looked up via File.ls!/1-equivalent (real_case_segment/2) rather than the caller's raw spelling being preserved verbatim -- so two differently-cased spellings of the SAME real file (e.g. lib/Foo.ex vs lib/foo.ex when only one really exists) canonicalize IDENTICALLY, the same way a /./-alias or a symlink alias already did. On a genuinely case-sensitive filesystem (Linux ext4) this is a strict no-op: the case-insensitive match this correction looks for cannot exist there because two distinct real files with different casing are two distinct real directory entries, and File.read_link/1 already proved the raw spelling itself resolves.

This module closes that gap as a real, reusable, independently-tested primitive rather than a one-off string-munge inlined into :admit -- see GgenIgniter.Reactors.ReconcileReactor's admit_pending/2 for the real wiring, GgenIgniter.PendingActuation's canonical_target field for where every planned actuation carries its own real identity, and GgenIgniter.Manifest/GgenIgniter.Receipt's call sites in ReconcileReactor for where the manifest's outputs keys and the receipt's files entries are now built from this same real identity rather than a raw path string.

Two-tier resolution, honestly bounded

canonicalize/2 resolves in two tiers, in order:

  1. Lexical normalization (Path.expand/2): relative-vs-absolute forms unified against base_dir, ./.. segments resolved, repeated separators collapsed. This ALONE is what closes the exact /./-alias reproducer above (neither colliding target need exist on disk yet for this tier to unify them).
  2. Real filesystem identity (symlink/realpath resolution): for every path SEGMENT that already exists on disk, this module walks it exactly like POSIX realpath(3) -- resolving any symlink encountered (including in an INTERMEDIATE directory segment, so a directory reached via two different real symlinked paths still collapses to one identity), with a bounded expansion budget so a symlink cycle can never loop forever.

Honest limit, stated plainly (per this module's own design brief): the moment resolution reaches a path segment that does not exist on disk at all, real filesystem resolution stops there -- the remaining, not-yet-existing suffix is appended to the already-resolved (real, or lexically-expanded, whichever tier got that far) prefix VERBATIM, after its own ./.. segments were already normalized by Path.expand/2 in tier 1. This is the best available identity for a target that does not exist yet: there is no real inode to resolve a not-yet-created file against. Concretely: a brand-new file inside an EXISTING (possibly symlinked) directory gets that directory's real identity as its prefix (stronger than bare lexical expansion); a brand-new file whose parent directory ALSO does not exist yet gets pure lexical normalization only, identical to Path.expand/2's own result. Two aliases of a not-yet- existing path that would only converge via a symlink CREATED after this function returns are, honestly, not detected -- there is no way to observe a filesystem fact that does not exist yet.

Summary

Functions

Resolves raw_path (relative or absolute, base_dir-relative when relative) into one canonical identity string -- see this module's moduledoc for the real two-tier algorithm and its honestly-disclosed limit for not-yet-existing targets.

Whether path_a and path_b (both resolved relative to the same base_dir) denote the SAME real artifact identity -- built directly on canonicalize/2, never a second, parallel comparison scheme.

Whether raw_path's real canonical identity remains INSIDE the authorized project root (base_dir, itself canonicalized the same way -- so a symlinked project root does not itself defeat this guard). Refuses (false) any path that escapes base_dir via .. traversal, an absolute path pointing elsewhere, or a symlink whose real target lands outside the root.

Functions

canonicalize(base_dir, raw_path)

@spec canonicalize(String.t(), String.t()) :: String.t()

Resolves raw_path (relative or absolute, base_dir-relative when relative) into one canonical identity string -- see this module's moduledoc for the real two-tier algorithm and its honestly-disclosed limit for not-yet-existing targets.

Two raw strings that are lexically different but denote the same real on-disk location (a redundant ./.. segment, a repeated separator, a relative-vs-absolute spelling of the same path, or -- for a path that already exists -- two different symlinked routes to the same real inode) canonicalize to the IDENTICAL string.

same_target?(base_dir, path_a, path_b)

@spec same_target?(String.t(), String.t(), String.t()) :: boolean()

Whether path_a and path_b (both resolved relative to the same base_dir) denote the SAME real artifact identity -- built directly on canonicalize/2, never a second, parallel comparison scheme.

within_root?(base_dir, raw_path)

@spec within_root?(String.t(), String.t()) :: boolean()

Whether raw_path's real canonical identity remains INSIDE the authorized project root (base_dir, itself canonicalized the same way -- so a symlinked project root does not itself defeat this guard). Refuses (false) any path that escapes base_dir via .. traversal, an absolute path pointing elsewhere, or a symlink whose real target lands outside the root.

base_dir itself is considered within its own root (within_root?(dir, dir) is true, matching File.rm_rf!/1-style "the root itself is a valid target" conventions elsewhere in this codebase); every other path must resolve to a REAL descendant of that root.