feat(fsext): stop upward lookup at a boundary directory

Christian Rocha and Charm Crush created

Add variants of the upward filesystem search that stop at a
caller-supplied boundary directory instead of walking all the way up
to the filesystem root or to $HOME. Callers that want to avoid adopting
matches from outside their project can now express that boundary
explicitly. Existing callers and behavior are unchanged.

Co-Authored-By: Charm Crush <crush@charm.land>

Change summary

internal/fsext/lookup.go      | 124 ++++++++++++++++++++++++++++++++++++
internal/fsext/lookup_test.go | 125 +++++++++++++++++++++++++++++++++++++
2 files changed, 249 insertions(+)

Detailed changes

internal/fsext/lookup.go 🔗

@@ -82,6 +82,81 @@ func LookupClosest(dir, target string) (string, bool) {
 	return found, err == nil && found != ""
 }
 
+// LookupClosestBounded behaves like LookupClosest but constrains the
+// upward search to stopDir. The walk inspects dir, then each ancestor up
+// to and including stopDir, then terminates regardless of whether the
+// target was found. Use this when the caller wants to avoid adopting
+// matches from outside a project boundary (for example a sibling
+// worktree or a parent project).
+//
+// If stopDir is empty, only dir itself is searched. If stopDir is not an
+// ancestor of dir, the walk still terminates at the filesystem root.
+// The $HOME and ownership safeguards from LookupClosest are preserved
+// as outer bounds.
+func LookupClosestBounded(dir, stopDir, target string) (string, bool) {
+	var found string
+
+	err := traverseUpBounded(dir, stopDir, func(cwd string, owner int) error {
+		fpath := filepath.Join(cwd, target)
+
+		err := probeEnt(fpath, owner)
+		if errors.Is(err, os.ErrNotExist) {
+			return nil
+		}
+
+		if err != nil {
+			return fmt.Errorf("error probing file %s: %w", fpath, err)
+		}
+
+		if cwd == home.Dir() {
+			return filepath.SkipAll
+		}
+
+		found = fpath
+		return filepath.SkipAll
+	})
+
+	return found, err == nil && found != ""
+}
+
+// LookupBounded behaves like Lookup but constrains the upward search to
+// stopDir. The walk inspects dir, then each ancestor up to and including
+// stopDir, then terminates. If stopDir is empty, only dir itself is
+// searched.
+func LookupBounded(dir, stopDir string, targets ...string) ([]string, error) {
+	if len(targets) == 0 {
+		return nil, nil
+	}
+
+	var found []string
+
+	err := traverseUpBounded(dir, stopDir, func(cwd string, owner int) error {
+		for _, target := range targets {
+			fpath := filepath.Join(cwd, target)
+			err := probeEnt(fpath, owner)
+
+			// skip to the next file on permission denied
+			if errors.Is(err, os.ErrNotExist) ||
+				errors.Is(err, os.ErrPermission) {
+				continue
+			}
+
+			if err != nil {
+				return fmt.Errorf("error probing file %s: %w", fpath, err)
+			}
+
+			found = append(found, fpath)
+		}
+
+		return nil
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	return found, nil
+}
+
 // traverseUp walks up from given directory up until filesystem root reached.
 // It passes absolute path of current directory and staring directory owner ID
 // to callback function. It is up to user to check ownership.
@@ -116,6 +191,55 @@ func traverseUp(dir string, walkFn func(dir string, owner int) error) error {
 	}
 }
 
+// traverseUpBounded walks up from dir, visiting each ancestor up to and
+// including stopDir, then terminates. If stopDir is empty, only dir
+// itself is visited; callers that want an unbounded walk should use
+// traverseUp instead. If stopDir is set but is not an ancestor of dir
+// the walk still stops at the filesystem root, so callers cannot
+// accidentally produce an infinite walk by passing a sibling path.
+func traverseUpBounded(dir, stopDir string, walkFn func(dir string, owner int) error) error {
+	cwd, err := filepath.Abs(dir)
+	if err != nil {
+		return fmt.Errorf("cannot convert CWD to absolute path: %w", err)
+	}
+
+	stop := cwd
+	if stopDir != "" {
+		stop, err = filepath.Abs(stopDir)
+		if err != nil {
+			return fmt.Errorf("cannot convert stop dir to absolute path: %w", err)
+		}
+	}
+
+	owner, err := Owner(dir)
+	if err != nil {
+		return fmt.Errorf("cannot get ownership: %w", err)
+	}
+
+	for {
+		err := walkFn(cwd, owner)
+		if err == nil || errors.Is(err, filepath.SkipDir) {
+			if cwd == stop {
+				return nil
+			}
+
+			parent := filepath.Dir(cwd)
+			if parent == cwd {
+				return nil
+			}
+
+			cwd = parent
+			continue
+		}
+
+		if errors.Is(err, filepath.SkipAll) {
+			return nil
+		}
+
+		return err
+	}
+}
+
 // probeEnt checks if entity at given path exists and belongs to given owner
 func probeEnt(fspath string, owner int) error {
 	_, err := os.Stat(fspath)

internal/fsext/lookup_test.go 🔗

@@ -353,6 +353,131 @@ func TestLookup(t *testing.T) {
 	})
 }
 
+func TestLookupClosestBounded(t *testing.T) {
+	t.Run("found in starting directory", func(t *testing.T) {
+		testDir := t.TempDir()
+
+		targetFile := filepath.Join(testDir, "target.txt")
+		require.NoError(t, os.WriteFile(targetFile, []byte("test"), 0o644))
+
+		foundPath, found := LookupClosestBounded(testDir, testDir, "target.txt")
+		require.True(t, found)
+		require.Equal(t, targetFile, foundPath)
+	})
+
+	t.Run("found at boundary directory", func(t *testing.T) {
+		boundary := t.TempDir()
+
+		subDir := filepath.Join(boundary, "subdir")
+		require.NoError(t, os.Mkdir(subDir, 0o755))
+
+		targetFile := filepath.Join(boundary, "target.txt")
+		require.NoError(t, os.WriteFile(targetFile, []byte("test"), 0o644))
+
+		foundPath, found := LookupClosestBounded(subDir, boundary, "target.txt")
+		require.True(t, found)
+		require.Equal(t, targetFile, foundPath)
+	})
+
+	t.Run("does not climb past boundary", func(t *testing.T) {
+		parent := t.TempDir()
+
+		// Target lives above the boundary.
+		require.NoError(t, os.WriteFile(filepath.Join(parent, "target.txt"), []byte("test"), 0o644))
+
+		boundary := filepath.Join(parent, "project")
+		require.NoError(t, os.Mkdir(boundary, 0o755))
+
+		subDir := filepath.Join(boundary, "subdir")
+		require.NoError(t, os.Mkdir(subDir, 0o755))
+
+		foundPath, found := LookupClosestBounded(subDir, boundary, "target.txt")
+		require.False(t, found)
+		require.Empty(t, foundPath)
+	})
+
+	t.Run("empty boundary searches only starting directory", func(t *testing.T) {
+		parent := t.TempDir()
+
+		require.NoError(t, os.WriteFile(filepath.Join(parent, "target.txt"), []byte("test"), 0o644))
+
+		subDir := filepath.Join(parent, "subdir")
+		require.NoError(t, os.Mkdir(subDir, 0o755))
+
+		foundPath, found := LookupClosestBounded(subDir, "", "target.txt")
+		require.False(t, found)
+		require.Empty(t, foundPath)
+	})
+
+	t.Run("empty boundary still finds in starting directory", func(t *testing.T) {
+		testDir := t.TempDir()
+
+		targetFile := filepath.Join(testDir, "target.txt")
+		require.NoError(t, os.WriteFile(targetFile, []byte("test"), 0o644))
+
+		foundPath, found := LookupClosestBounded(testDir, "", "target.txt")
+		require.True(t, found)
+		require.Equal(t, targetFile, foundPath)
+	})
+}
+
+func TestLookupBounded(t *testing.T) {
+	t.Run("returns matches at and below boundary", func(t *testing.T) {
+		boundary := t.TempDir()
+
+		subDir := filepath.Join(boundary, "subdir")
+		require.NoError(t, os.Mkdir(subDir, 0o755))
+
+		atBoundary := filepath.Join(boundary, "target.txt")
+		atSub := filepath.Join(subDir, "target.txt")
+		require.NoError(t, os.WriteFile(atBoundary, []byte("a"), 0o644))
+		require.NoError(t, os.WriteFile(atSub, []byte("b"), 0o644))
+
+		found, err := LookupBounded(subDir, boundary, "target.txt")
+		require.NoError(t, err)
+		require.Len(t, found, 2)
+		require.Contains(t, found, atBoundary)
+		require.Contains(t, found, atSub)
+	})
+
+	t.Run("ignores matches above boundary", func(t *testing.T) {
+		parent := t.TempDir()
+
+		require.NoError(t, os.WriteFile(filepath.Join(parent, "target.txt"), []byte("nope"), 0o644))
+
+		boundary := filepath.Join(parent, "project")
+		require.NoError(t, os.Mkdir(boundary, 0o755))
+
+		subDir := filepath.Join(boundary, "subdir")
+		require.NoError(t, os.Mkdir(subDir, 0o755))
+
+		// Target lives only above the boundary.
+		found, err := LookupBounded(subDir, boundary, "target.txt")
+		require.NoError(t, err)
+		require.Empty(t, found)
+	})
+
+	t.Run("empty boundary searches only starting directory", func(t *testing.T) {
+		parent := t.TempDir()
+
+		require.NoError(t, os.WriteFile(filepath.Join(parent, "target.txt"), []byte("nope"), 0o644))
+
+		subDir := filepath.Join(parent, "subdir")
+		require.NoError(t, os.Mkdir(subDir, 0o755))
+
+		found, err := LookupBounded(subDir, "", "target.txt")
+		require.NoError(t, err)
+		require.Empty(t, found)
+	})
+
+	t.Run("no targets returns nil", func(t *testing.T) {
+		dir := t.TempDir()
+		found, err := LookupBounded(dir, dir)
+		require.NoError(t, err)
+		require.Empty(t, found)
+	})
+}
+
 func TestProbeEnt(t *testing.T) {
 	t.Run("existing file with correct owner", func(t *testing.T) {
 		tempDir := t.TempDir()