lookup.go 3.3 KB

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