1package fsext
 2
 3import (
 4	"errors"
 5	"os"
 6	"path/filepath"
 7
 8	"github.com/charmbracelet/crush/internal/home"
 9)
10
11// SearchParent searches for a target file or directory starting from dir
12// and walking up the directory tree until found or root or home is reached.
13// It also checks the ownership of directories to ensure that the search does
14// not cross ownership boundaries.
15// Returns the full path to the target if found, empty string and false otherwise.
16// The search includes the starting directory itself.
17func SearchParent(dir, target string) (string, bool) {
18	absDir, err := filepath.Abs(dir)
19	if err != nil {
20		return "", false
21	}
22
23	path := filepath.Join(absDir, target)
24	if _, err := os.Stat(path); err == nil {
25		return path, true
26	} else if !errors.Is(err, os.ErrNotExist) {
27		return "", false
28	}
29
30	previousParent := absDir
31	previousOwner, err := Owner(previousParent)
32	if err != nil {
33		return "", false
34	}
35
36	for {
37		parent := filepath.Dir(previousParent)
38		if parent == previousParent || parent == home.Dir() {
39			return "", false
40		}
41
42		parentOwner, err := Owner(parent)
43		if err != nil {
44			return "", false
45		}
46		if parentOwner != previousOwner {
47			return "", false
48		}
49
50		path := filepath.Join(parent, target)
51		if _, err := os.Stat(path); err == nil {
52			return path, true
53		} else if !errors.Is(err, os.ErrNotExist) {
54			return "", false
55		}
56
57		previousParent = parent
58		previousOwner = parentOwner
59	}
60}