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}