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:
- Lexical normalization (
Path.expand/2): relative-vs-absolute forms unified againstbase_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). - 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
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.
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.
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.