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}