From 5eac402861c012121627e9a666ef667750519a20 Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Mon, 10 Aug 2026 11:07:14 -0400 Subject: [PATCH] refactor: deduplicate alias payload calculation --- internal/application/apply/execute_aliases.go | 40 ++--------------- internal/application/apply/verify.go | 6 ++- internal/pathsafe/alias.go | 38 ++++++++++++++++ internal/reconcile/classify_alias.go | 33 +------------- internal/routes/activate.go | 45 ------------------- internal/routes/aliases_test.go | 10 +++-- 6 files changed, 55 insertions(+), 117 deletions(-) create mode 100644 internal/pathsafe/alias.go diff --git a/internal/application/apply/execute_aliases.go b/internal/application/apply/execute_aliases.go index 78e3e36..4d855d8 100644 --- a/internal/application/apply/execute_aliases.go +++ b/internal/application/apply/execute_aliases.go @@ -2,9 +2,7 @@ package apply import ( "context" - "strings" - "github.com/alyraffauf/cattery/internal/deployment" "github.com/alyraffauf/cattery/internal/failure" "github.com/alyraffauf/cattery/internal/filesystem" "github.com/alyraffauf/cattery/internal/pathsafe" @@ -65,7 +63,10 @@ func (service *Service) realizeAlias(ctx context.Context, job aliasJob) (ItemRes if err != nil { return aliasRecord(job, StatusPartial), failure.New(failure.Operational, "apply: freeze alias "+job.action.TargetPath, err) } - payload, err := aliasPayload(job.candidate.record.Alias) + payload, err := pathsafe.RelativeAliasPayload( + job.candidate.record.Alias.CanonicalTargetRelativePath, + job.candidate.record.Alias.AliasRelativePath, + ) if err != nil { return aliasRecord(job, StatusPartial), failure.New(failure.InvalidInput, "apply: payload for "+job.action.TargetPath, err) } @@ -135,36 +136,3 @@ func aliasRecord(job aliasJob, status ItemStatus) ItemResult { Kind: job.action.Kind, } } - -// aliasPayload derives the exact relative payload one alias link must -// carry, mirroring the routes activation contract (PLAN.md Section 5.4). -func aliasPayload(alias deployment.Alias) (string, error) { - canonical, err := pathsafe.Segments(alias.CanonicalTargetRelativePath) - if err != nil { - return "", err - } - link, err := pathsafe.Segments(alias.AliasRelativePath) - if err != nil { - return "", err - } - parent := link[:len(link)-1] - common := commonPrefix(canonical, parent) - remaining := canonical[common:] - if len(remaining) == 0 { - return "", failure.New(failure.InvalidInput, "apply: alias descends into its canonical target", nil) - } - backtrack := len(parent) - common - if backtrack == 0 { - return strings.Join(remaining, "/"), nil - } - return strings.Repeat("../", backtrack) + strings.Join(remaining, "/"), nil -} - -// commonPrefix counts the shared leading segments of two path lists. -func commonPrefix(first, second []string) int { - shared := 0 - for shared < len(first) && shared < len(second) && first[shared] == second[shared] { - shared++ - } - return shared -} diff --git a/internal/application/apply/verify.go b/internal/application/apply/verify.go index 6817a5e..f035c90 100644 --- a/internal/application/apply/verify.go +++ b/internal/application/apply/verify.go @@ -5,6 +5,7 @@ import ( "github.com/alyraffauf/cattery/internal/deployment" "github.com/alyraffauf/cattery/internal/failure" + "github.com/alyraffauf/cattery/internal/pathsafe" "github.com/alyraffauf/cattery/internal/reconcile" ) @@ -92,7 +93,10 @@ func (service *Service) verifySource(ctx context.Context, candidate Candidate) ( // verifyAlias requires the target to be a symlink carrying the exact // derived payload. func (service *Service) verifyAlias(candidate Candidate, target reconcile.TargetSnapshot) (bool, error) { - payload, err := aliasPayload(candidate.record.Alias) + payload, err := pathsafe.RelativeAliasPayload( + candidate.record.Alias.CanonicalTargetRelativePath, + candidate.record.Alias.AliasRelativePath, + ) if err != nil { return false, err } diff --git a/internal/pathsafe/alias.go b/internal/pathsafe/alias.go new file mode 100644 index 0000000..8c947e8 --- /dev/null +++ b/internal/pathsafe/alias.go @@ -0,0 +1,38 @@ +package pathsafe + +import ( + "fmt" + "strings" +) + +// RelativeAliasPayload returns the relative symlink payload from alias's +// parent directory to canonical. +func RelativeAliasPayload(canonical, alias string) (string, error) { + canonicalSegments, err := Segments(canonical) + if err != nil { + return "", err + } + aliasSegments, err := Segments(alias) + if err != nil { + return "", err + } + parent := aliasSegments[:len(aliasSegments)-1] + common := commonPrefix(canonicalSegments, parent) + remaining := canonicalSegments[common:] + if len(remaining) == 0 { + return "", fmt.Errorf("pathsafe: alias %q descends into canonical %q", alias, canonical) + } + backtrack := len(parent) - common + if backtrack == 0 { + return strings.Join(remaining, "/"), nil + } + return strings.Repeat("../", backtrack) + strings.Join(remaining, "/"), nil +} + +func commonPrefix(first, second []string) int { + shared := 0 + for shared < len(first) && shared < len(second) && first[shared] == second[shared] { + shared++ + } + return shared +} diff --git a/internal/reconcile/classify_alias.go b/internal/reconcile/classify_alias.go index 0629481..d520c97 100644 --- a/internal/reconcile/classify_alias.go +++ b/internal/reconcile/classify_alias.go @@ -1,9 +1,6 @@ package reconcile import ( - "fmt" - "strings" - "github.com/alyraffauf/cattery/internal/deployment" "github.com/alyraffauf/cattery/internal/pathsafe" ) @@ -121,41 +118,13 @@ func activeAliasRow(record Evaluation) bool { // for canonical, or the empty string when the paths cannot describe a valid // alias, so a real link can never match an invalid declaration. func payloadFor(canonical, alias string) string { - payload, err := relativePayload(canonical, alias) + payload, err := pathsafe.RelativeAliasPayload(canonical, alias) if err != nil { return "" } return payload } -// relativePayload computes the exact relative symlink payload from the alias -// destination's parent directory to the canonical target, mirroring the -// route-activation derivation of PLAN.md Section 5.4 without importing it. -func relativePayload(canonical, alias string) (string, error) { - canonicalSegments, err := pathsafe.Segments(canonical) - if err != nil { - return "", err - } - aliasSegments, err := pathsafe.Segments(alias) - if err != nil { - return "", err - } - parent := aliasSegments[:len(aliasSegments)-1] - common := 0 - for common < len(canonicalSegments) && common < len(parent) && canonicalSegments[common] == parent[common] { - common++ - } - remaining := canonicalSegments[common:] - if len(remaining) == 0 { - return "", fmt.Errorf("reconcile: alias %q descends into canonical %q", alias, canonical) - } - up := len(parent) - common - if up == 0 { - return strings.Join(remaining, "/"), nil - } - return strings.Repeat("../", up) + strings.Join(remaining, "/"), nil -} - // aliasWithPath attaches the evaluation target path to a bare outcome. func aliasWithPath(candidate AliasClassification, record Evaluation) AliasClassification { candidate.TargetPath = record.TargetPath diff --git a/internal/routes/activate.go b/internal/routes/activate.go index 89ef884..b834870 100644 --- a/internal/routes/activate.go +++ b/internal/routes/activate.go @@ -2,10 +2,8 @@ package routes import ( "fmt" - "strings" "github.com/alyraffauf/cattery/internal/deployment" - "github.com/alyraffauf/cattery/internal/pathsafe" ) // Activate resolves the route declarations active for one platform against @@ -87,46 +85,3 @@ func canonicalSet(canonical []string) map[string]bool { } return managed } - -// AliasPayload computes the exact relative symlink payload for the alias at -// destination pointing at canonical (PLAN.md Section 5.4): the payload is -// relative from the alias destination's parent directory, never absolute, -// and never needs to climb above the home root. Both paths must be valid -// HOME-relative paths. -// -// AliasPayload rejects only the case where the canonical segments that remain -// after the common prefix with the alias's parent directory are empty — i.e. -// when canonical is the alias parent or one of its ancestors, which would -// leave no target to point at. A self-referential single-segment alias where -// canonical == alias is not rejected here; that case is caught upstream by -// Activate (which errors when destination == canonical). -func AliasPayload(canonical, alias string) (string, error) { - canonicalSegments, err := pathsafe.Segments(canonical) - if err != nil { - return "", err - } - aliasSegments, err := pathsafe.Segments(alias) - if err != nil { - return "", err - } - parent := aliasSegments[:len(aliasSegments)-1] - common := commonPrefix(canonicalSegments, parent) - remaining := canonicalSegments[common:] - if len(remaining) == 0 { - return "", fmt.Errorf("routes: alias %q descends into canonical %q", alias, canonical) - } - up := len(parent) - common - if up == 0 { - return strings.Join(remaining, "/"), nil - } - return strings.Repeat("../", up) + strings.Join(remaining, "/"), nil -} - -func commonPrefix(first, second []string) int { - length := min(len(first), len(second)) - common := 0 - for common < length && first[common] == second[common] { - common++ - } - return common -} diff --git a/internal/routes/aliases_test.go b/internal/routes/aliases_test.go index 7d5d6ad..501214e 100644 --- a/internal/routes/aliases_test.go +++ b/internal/routes/aliases_test.go @@ -1,6 +1,10 @@ package routes -import "testing" +import ( + "testing" + + "github.com/alyraffauf/cattery/internal/pathsafe" +) func TestAliasDeclaration(t *testing.T) { scenarios := []struct { @@ -80,7 +84,7 @@ type payloadCase struct { func assertPayload(t *testing.T, scenario payloadCase) { t.Helper() - got, err := AliasPayload(scenario.canonical, scenario.alias) + got, err := pathsafe.RelativeAliasPayload(scenario.canonical, scenario.alias) if err != nil { t.Fatalf("payload for %q -> %q: %v", scenario.alias, scenario.canonical, err) } @@ -91,7 +95,7 @@ func assertPayload(t *testing.T, scenario payloadCase) { func assertPayloadError(t *testing.T, scenario payloadCase) { t.Helper() - payload, err := AliasPayload(scenario.canonical, scenario.alias) + payload, err := pathsafe.RelativeAliasPayload(scenario.canonical, scenario.alias) if err == nil { t.Fatalf("payload for %q -> %q = %q, want error", scenario.alias, scenario.canonical, payload) } -- 2.51.2