1package fsext
2
3import (
4 "errors"
5 "fmt"
6 "os"
7 "path/filepath"
8
9 "github.com/charmbracelet/crush/internal/home"
10)
11
12// Lookup searches for a target files or directories starting from dir
13// and walking up the directory tree until filesystem root is reached.
14// It also checks the ownership of files to ensure that the search does
15// not cross ownership boundaries. It skips ownership mismatches without
16// errors.
17// Returns full paths to fount targets.
18// The search includes the starting directory itself.
19func Lookup(dir string, targets ...string) ([]string, error) {
20 if len(targets) == 0 {
21 return nil, nil
22 }
23
24 var found []string
25
26 err := traverseUp(dir, func(cwd string, owner int) error {
27 for _, target := range targets {
28 fpath := filepath.Join(cwd, target)
29 err := probeEnt(fpath, owner)
30
31 // skip to the next file on permission denied
32 if errors.Is(err, os.ErrNotExist) ||
33 errors.Is(err, os.ErrPermission) {
34 continue
35 }
36
37 if err != nil {
38 return fmt.Errorf("error probing file %s: %w", fpath, err)
39 }
40
41 found = append(found, fpath)
42 }
43
44 return nil
45 })
46 if err != nil {
47 return nil, err
48 }
49
50 return found, nil
51}
52
53// LookupClosest searches for a target file or directory starting from dir
54// and walking up the directory tree until found or root or home is reached.
55// It also checks the ownership of files to ensure that the search does
56// not cross ownership boundaries.
57// Returns the full path to the target if found, empty string and false otherwise.
58// The search includes the starting directory itself.
59func LookupClosest(dir, target string) (string, bool) {
60 var found string
61
62 err := traverseUp(dir, func(cwd string, owner int) error {
63 fpath := filepath.Join(cwd, target)
64
65 err := probeEnt(fpath, owner)
66 if errors.Is(err, os.ErrNotExist) {
67 return nil
68 }
69
70 if err != nil {
71 return fmt.Errorf("error probing file %s: %w", fpath, err)
72 }
73
74 if cwd == home.Dir() {
75 return filepath.SkipAll
76 }
77
78 found = fpath
79 return filepath.SkipAll
80 })
81
82 return found, err == nil && found != ""
83}
84
85// traverseUp walks up from given directory up until filesystem root reached.
86// It passes absolute path of current directory and staring directory owner ID
87// to callback function. It is up to user to check ownership.
88func traverseUp(dir string, walkFn func(dir string, owner int) error) error {
89 cwd, err := filepath.Abs(dir)
90 if err != nil {
91 return fmt.Errorf("cannot convert CWD to absolute path: %w", err)
92 }
93
94 owner, err := Owner(dir)
95 if err != nil {
96 return fmt.Errorf("cannot get ownership: %w", err)
97 }
98
99 for {
100 err := walkFn(cwd, owner)
101 if err == nil || errors.Is(err, filepath.SkipDir) {
102 parent := filepath.Dir(cwd)
103 if parent == cwd {
104 return nil
105 }
106
107 cwd = parent
108 continue
109 }
110
111 if errors.Is(err, filepath.SkipAll) {
112 return nil
113 }
114
115 return err
116 }
117}
118
119// probeEnt checks if entity at given path exists and belongs to given owner
120func probeEnt(fspath string, owner int) error {
121 _, err := os.Stat(fspath)
122 if err != nil {
123 return fmt.Errorf("cannot stat %s: %w", fspath, err)
124 }
125
126 // special case for ownership check bypass
127 if owner == -1 {
128 return nil
129 }
130
131 fowner, err := Owner(fspath)
132 if err != nil {
133 return fmt.Errorf("cannot get ownership for %s: %w", fspath, err)
134 }
135
136 if fowner != owner {
137 return os.ErrPermission
138 }
139
140 return nil
141}