...
1 package devices
2
3 import (
4 "io/fs"
5 "os"
6 "path/filepath"
7 "slices"
8 "strings"
9 )
10
11
12 var isSymlinkFn = func(fileInfo fs.FileInfo) bool {
13 return fileInfo.Mode()&fs.ModeSymlink != 0
14 }
15
16 type opts struct {
17 matchAll bool
18 matchers []*Matcher
19 }
20
21 type Option func(*opts)
22
23
24 func WithMatchAll(matchAll bool) Option {
25 return func(o *opts) {
26 o.matchAll = matchAll
27 }
28 }
29
30
31 func WithMatchers(matchers ...*Matcher) Option {
32 return func(o *opts) {
33 o.matchers = matchers
34 }
35 }
36
37
38
39
40 func Find(options ...Option) (map[string]Device, error) {
41 var (
42 devices = map[string]Device{}
43 err error
44 )
45
46 renderedOpts := &opts{}
47 for _, o := range options {
48 o(renderedOpts)
49 }
50
51 if renderedOpts.matchAll {
52 renderedOpts.matchers = []*Matcher{
53 NewMatcher().MatchAll(),
54 }
55 }
56
57
58 searchPaths := []string{}
59 for _, m := range renderedOpts.matchers {
60 searchPaths = append(searchPaths, m.SearchPaths()...)
61 }
62 slices.Sort(searchPaths)
63 searchPaths = slices.Compact(searchPaths)
64 if slices.Contains(searchPaths, sysDevicePath) {
65 searchPaths = []string{sysDevicePath}
66 }
67
68 for _, sPath := range searchPaths {
69 devices, err = searchPath(sPath, devices, renderedOpts.matchers...)
70 if err != nil {
71 return devices, err
72 }
73 }
74 return devices, nil
75 }
76
77 func searchPath(searchPath string, devices map[string]Device, matchers ...*Matcher) (map[string]Device, error) {
78 if err := filepath.Walk(searchPath, func(path string, _ os.FileInfo, err error) error {
79 if err != nil {
80 return nil
81 }
82
83 realPath, err := fetchRealPath(path)
84 if err != nil {
85 return err
86 }
87
88 realPathInfo, err := os.Stat(realPath)
89 if err != nil {
90 if os.IsNotExist(err) {
91 return nil
92 }
93 return err
94 }
95
96 if !realPathInfo.IsDir() {
97 return nil
98 }
99
100 if realPath == searchPath || path == searchPath {
101 return nil
102 }
103
104 if _, err := os.Stat(filepath.Join(realPath, "uevent")); err != nil || os.IsNotExist(err) {
105 return nil
106 }
107
108 newDevice, err := New(realPath)
109 if err != nil {
110 return nil
111 }
112
113 if _, exists := devices[realPath]; exists {
114 return nil
115 }
116
117 if hasAscendant(newDevice.Path(), devices) {
118 devices[newDevice.Path()] = newDevice
119 return nil
120 }
121
122 if len(matchers) == 0 {
123 devices[newDevice.Path()] = newDevice
124 return nil
125 }
126
127 for _, m := range matchers {
128 if m.MatchDevice(newDevice) {
129 devices[newDevice.Path()] = newDevice
130 return nil
131 }
132 }
133 return nil
134 }); err != nil {
135 return devices, err
136 }
137 return devices, nil
138 }
139
140
141 func fetchRealPath(path string) (string, error) {
142 fileInfo, err := os.Lstat(path)
143 if err != nil {
144 return "", err
145 }
146
147 if isSymlinkFn(fileInfo) {
148 newPath, err := filepath.EvalSymlinks(path)
149 if err != nil {
150 return "", err
151 }
152 return filepath.Abs(newPath)
153 }
154 return path, nil
155 }
156
157
158 func hasAscendant(path string, devices map[string]Device) bool {
159 pathSplit := strings.Split(path, "/")
160 for idx := len(pathSplit) - 1; idx >= 0; idx-- {
161 searchPath := strings.Join(pathSplit[:idx], "/")
162 if slices.Contains([]string{sysDevicePath, sysBusPath, sysClassPath}, searchPath) {
163 return false
164 }
165 if _, ok := devices[searchPath]; ok {
166 return true
167 }
168 }
169 return false
170 }
171
View as plain text