package devices import ( "io/fs" "os" "path/filepath" "slices" "strings" ) // mock file mode for symlinks var isSymlinkFn = func(fileInfo fs.FileInfo) bool { return fileInfo.Mode()&fs.ModeSymlink != 0 } type opts struct { matchAll bool matchers []*Matcher } type Option func(*opts) // Match all devices func WithMatchAll(matchAll bool) Option { return func(o *opts) { o.matchAll = matchAll } } // Matchers to search with func WithMatchers(matchers ...*Matcher) Option { return func(o *opts) { o.matchers = matchers } } // Find walks rootPath and returns a mapped list of devices that // match to the provided matcher. Find will // search /sys/devices or /sys/class if matchers specify a subsystem. func Find(options ...Option) (map[string]Device, error) { var ( devices = map[string]Device{} err error ) renderedOpts := &opts{} for _, o := range options { o(renderedOpts) } if renderedOpts.matchAll { renderedOpts.matchers = []*Matcher{ NewMatcher().MatchAll(), } } // set search path from subsystem specified in matchers searchPaths := []string{} for _, m := range renderedOpts.matchers { searchPaths = append(searchPaths, m.SearchPaths()...) } slices.Sort(searchPaths) searchPaths = slices.Compact(searchPaths) if slices.Contains(searchPaths, sysDevicePath) { searchPaths = []string{sysDevicePath} } for _, sPath := range searchPaths { devices, err = searchPath(sPath, devices, renderedOpts.matchers...) if err != nil { return devices, err } } return devices, nil } func searchPath(searchPath string, devices map[string]Device, matchers ...*Matcher) (map[string]Device, error) { if err := filepath.Walk(searchPath, func(path string, _ os.FileInfo, err error) error { if err != nil { return nil } realPath, err := fetchRealPath(path) if err != nil { return err } realPathInfo, err := os.Stat(realPath) if err != nil { if os.IsNotExist(err) { return nil } return err } if !realPathInfo.IsDir() { return nil } if realPath == searchPath || path == searchPath { return nil } if _, err := os.Stat(filepath.Join(realPath, "uevent")); err != nil || os.IsNotExist(err) { return nil } newDevice, err := New(realPath) if err != nil { return nil } if _, exists := devices[realPath]; exists { return nil // already added } if hasAscendant(newDevice.Path(), devices) { devices[newDevice.Path()] = newDevice return nil } if len(matchers) == 0 { devices[newDevice.Path()] = newDevice return nil } for _, m := range matchers { if m.MatchDevice(newDevice) { devices[newDevice.Path()] = newDevice return nil } } return nil }); err != nil { return devices, err } return devices, nil } // fetch real path returns the true path, handling symlinks func fetchRealPath(path string) (string, error) { fileInfo, err := os.Lstat(path) if err != nil { return "", err } if isSymlinkFn(fileInfo) { newPath, err := filepath.EvalSymlinks(path) if err != nil { return "", err } return filepath.Abs(newPath) } return path, nil } // hasAscendant will search devices for an ascendant device given a path func hasAscendant(path string, devices map[string]Device) bool { pathSplit := strings.Split(path, "/") for idx := len(pathSplit) - 1; idx >= 0; idx-- { searchPath := strings.Join(pathSplit[:idx], "/") if slices.Contains([]string{sysDevicePath, sysBusPath, sysClassPath}, searchPath) { return false } if _, ok := devices[searchPath]; ok { return true } } return false }