From d59180cf5730aa96bd9376b4a0cf8abb39012e16 Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Mon, 10 Aug 2026 12:18:44 -0400 Subject: [PATCH] Canonicalize home during repository selection --- internal/selection/repository.go | 16 ++++++++++------ internal/selection/repository_test.go | 22 ++++++++++++++++++++++ 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/internal/selection/repository.go b/internal/selection/repository.go index b190254..3c23859 100644 --- a/internal/selection/repository.go +++ b/internal/selection/repository.go @@ -35,7 +35,7 @@ type RepositoryResolver struct { defaults *state.Store } -// NewRepositoryResolver constructs a resolver bound to the canonical home +// NewRepositoryResolver constructs a resolver bound to the raw home path // and the read-only default lookup. Construction performs no filesystem or // state access. func NewRepositoryResolver(home string, defaults *state.Store) *RepositoryResolver { @@ -45,27 +45,31 @@ func NewRepositoryResolver(home string, defaults *state.Store) *RepositoryResolv // Resolve applies explicit path, then environment, then the canonical-home // default, failing with instructions when nothing selects a repository. func (resolver *RepositoryResolver) Resolve(request RepositoryRequest) (state.Repository, error) { + home, err := pathsafe.CanonicalRoot(resolver.home) + if err != nil { + return state.Repository{}, err + } if request.ExplicitSet { if request.RawExplicit == "" { return state.Repository{}, fmt.Errorf("selection: explicit repository path is empty") } - return resolver.canonical(request.RawExplicit, request.WorkingDir) + return resolver.canonical(request.RawExplicit, request.WorkingDir, home) } if request.EnvSet { if request.RawEnv == "" { return state.Repository{}, fmt.Errorf("selection: CATTERY_REPO is empty") } - return resolver.canonical(request.RawEnv, request.WorkingDir) + return resolver.canonical(request.RawEnv, request.WorkingDir, home) } if err := resolver.defaults.EnsureAcquired(); err != nil { return state.Repository{}, err } - return resolver.defaults.DefaultRepository(resolver.home) + return resolver.defaults.DefaultRepository(home) } // canonical resolves raw against the working directory and returns the // canonical pair under the resolver's home. -func (resolver *RepositoryResolver) canonical(raw, workingDir string) (state.Repository, error) { +func (resolver *RepositoryResolver) canonical(raw, workingDir, home string) (state.Repository, error) { path := raw if !filepath.IsAbs(raw) { path = filepath.Join(workingDir, raw) @@ -74,5 +78,5 @@ func (resolver *RepositoryResolver) canonical(raw, workingDir string) (state.Rep if err != nil { return state.Repository{}, err } - return state.Repository{RootPath: root, HomePath: resolver.home}, nil + return state.Repository{RootPath: root, HomePath: home}, nil } diff --git a/internal/selection/repository_test.go b/internal/selection/repository_test.go index ff2a97a..4e98539 100644 --- a/internal/selection/repository_test.go +++ b/internal/selection/repository_test.go @@ -24,6 +24,7 @@ func TestRepositorySelection(t *testing.T) { {"absent default fails", testSelectionAbsentDefault}, {"two homes keep separate defaults", testSelectionTwoHomes}, {"canonical results", testSelectionCanonical}, + {"canonical home", testSelectionCanonicalHome}, {"no implicit registration", testSelectionNoRegistration}, } for _, scenario := range scenarios { @@ -183,6 +184,27 @@ func testSelectionCanonical(t *testing.T) { } } +func testSelectionCanonicalHome(t *testing.T) { + fixture := database.New(t) + realHome := filepath.Join(fixture.Root, "real-home") + if err := os.Mkdir(realHome, 0o700); err != nil { + t.Fatalf("make real home: %v", err) + } + linkedHome := filepath.Join(fixture.Root, "linked-home") + if err := os.Symlink(realHome, linkedHome); err != nil { + t.Fatalf("make linked home: %v", err) + } + repository := filepath.Join(fixture.Root, "repository") + resolver := NewRepositoryResolver(linkedHome, fixture.Store) + result, err := resolver.Resolve(RepositoryRequest{RawExplicit: repository, ExplicitSet: true, WorkingDir: t.TempDir()}) + if err != nil { + t.Fatalf("resolve linked home: %v", err) + } + if result.HomePath != realHome { + t.Fatalf("home = %q, want %q", result.HomePath, realHome) + } +} + func testSelectionNoRegistration(t *testing.T) { resolver, fixture := newFixtureResolver(t) repo := filepath.Join(fixture.Root, "repo") -- 2.51.2