From 390e5747ea5fa388505fdb1024c99505f5cafc0d Mon Sep 17 00:00:00 2001 From: Evan Lezar Date: Wed, 9 Mar 2022 17:51:51 +0200 Subject: [PATCH] Add lookup abstraction for locating executable files Signed-off-by: Evan Lezar --- internal/lookup/file.go | 85 ++++++++++++++++++++++++++++++ internal/lookup/locator.go | 24 +++++++++ internal/lookup/locator_mock.go | 77 +++++++++++++++++++++++++++ internal/lookup/path.go | 93 +++++++++++++++++++++++++++++++++ 4 files changed, 279 insertions(+) create mode 100644 internal/lookup/file.go create mode 100644 internal/lookup/locator.go create mode 100644 internal/lookup/locator_mock.go create mode 100644 internal/lookup/path.go diff --git a/internal/lookup/file.go b/internal/lookup/file.go new file mode 100644 index 00000000..c9008f93 --- /dev/null +++ b/internal/lookup/file.go @@ -0,0 +1,85 @@ +/* +# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +*/ + +package lookup + +import ( + "fmt" + "os" + "path/filepath" + + log "github.com/sirupsen/logrus" +) + +// file can be used to locate file (or file-like elements) at a specified set of +// prefixes. The validity of a file is determined by a filter function. +type file struct { + logger *log.Logger + prefixes []string + filter func(string) error +} + +// NewFileLocator creates a Locator that can be used to find files at the specified root. A logger +// can also be specified. +func NewFileLocator(logger *log.Logger, root string) Locator { + l := newFileLocator(logger, root) + + return &l +} + +func newFileLocator(logger *log.Logger, root string) file { + return file{ + logger: logger, + prefixes: []string{root}, + filter: assertFile, + } +} + +var _ Locator = (*file)(nil) + +// Locate attempts to find the specified file. All prefixes are searched and any matching +// candidates are returned. If no matches are found, an error is returned. +func (p file) Locate(filename string) ([]string, error) { + var filenames []string + for _, prefix := range p.prefixes { + candidate := filepath.Join(prefix, filename) + p.logger.Debugf("Checking candidate '%v'", candidate) + err := p.filter(candidate) + if err != nil { + p.logger.Debugf("Candidate '%v' does not meet requirements: %v", candidate, err) + continue + } + filenames = append(filenames, candidate) + } + if len(filename) == 0 { + return nil, fmt.Errorf("file %v not found", filename) + } + return filenames, nil +} + +// assertFile checks whether the specified path is a regular file +func assertFile(filename string) error { + info, err := os.Stat(filename) + if err != nil { + return fmt.Errorf("error getting info for %v: %v", filename, err) + } + + if info.IsDir() { + return fmt.Errorf("specified path '%v' is a directory", filename) + } + + return nil +} diff --git a/internal/lookup/locator.go b/internal/lookup/locator.go new file mode 100644 index 00000000..871e1b02 --- /dev/null +++ b/internal/lookup/locator.go @@ -0,0 +1,24 @@ +/* +# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +*/ + +package lookup + +//go:generate moq -stub -out locator_mock.go . Locator + +// Locator defines the interface for locating files on a system. +type Locator interface { + Locate(string) ([]string, error) +} diff --git a/internal/lookup/locator_mock.go b/internal/lookup/locator_mock.go new file mode 100644 index 00000000..0c50f345 --- /dev/null +++ b/internal/lookup/locator_mock.go @@ -0,0 +1,77 @@ +// Code generated by moq; DO NOT EDIT. +// github.com/matryer/moq + +package lookup + +import ( + "sync" +) + +// Ensure, that LocatorMock does implement Locator. +// If this is not the case, regenerate this file with moq. +var _ Locator = &LocatorMock{} + +// LocatorMock is a mock implementation of Locator. +// +// func TestSomethingThatUsesLocator(t *testing.T) { +// +// // make and configure a mocked Locator +// mockedLocator := &LocatorMock{ +// LocateFunc: func(s string) ([]string, error) { +// panic("mock out the Locate method") +// }, +// } +// +// // use mockedLocator in code that requires Locator +// // and then make assertions. +// +// } +type LocatorMock struct { + // LocateFunc mocks the Locate method. + LocateFunc func(s string) ([]string, error) + + // calls tracks calls to the methods. + calls struct { + // Locate holds details about calls to the Locate method. + Locate []struct { + // S is the s argument value. + S string + } + } + lockLocate sync.RWMutex +} + +// Locate calls LocateFunc. +func (mock *LocatorMock) Locate(s string) ([]string, error) { + callInfo := struct { + S string + }{ + S: s, + } + mock.lockLocate.Lock() + mock.calls.Locate = append(mock.calls.Locate, callInfo) + mock.lockLocate.Unlock() + if mock.LocateFunc == nil { + var ( + stringsOut []string + errOut error + ) + return stringsOut, errOut + } + return mock.LocateFunc(s) +} + +// LocateCalls gets all the calls that were made to Locate. +// Check the length with: +// len(mockedLocator.LocateCalls()) +func (mock *LocatorMock) LocateCalls() []struct { + S string +} { + var calls []struct { + S string + } + mock.lockLocate.RLock() + calls = mock.calls.Locate + mock.lockLocate.RUnlock() + return calls +} diff --git a/internal/lookup/path.go b/internal/lookup/path.go new file mode 100644 index 00000000..9dffa4c5 --- /dev/null +++ b/internal/lookup/path.go @@ -0,0 +1,93 @@ +/* +# Copyright (c) 2021, NVIDIA CORPORATION. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +*/ + +package lookup + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + log "github.com/sirupsen/logrus" +) + +const ( + envPath = "PATH" +) + +var defaultPaths = []string{"/usr/local/sbin", "/usr/local/bin", "/usr/sbin", "/usr/bin", "/sbin", "/bin"} + +type path struct { + file +} + +// NewPathLocator creates a locator to fine executable files in the path. A logger can also be specified. +func NewPathLocator(logger *log.Logger, root string) Locator { + pathEnv := os.Getenv(envPath) + paths := filepath.SplitList(pathEnv) + + if root != "" { + paths = append(paths, defaultPaths...) + } + + var prefixes []string + for _, dir := range paths { + prefixes = append(prefixes, filepath.Join(root, dir)) + } + l := path{ + file: file{ + logger: logger, + prefixes: prefixes, + filter: assertExecutable, + }, + } + return &l +} + +var _ Locator = (*path)(nil) + +// Locate finds executable files in the path. If a relative or absolute path is specified, the prefix paths are not considered. +func (p path) Locate(filename string) ([]string, error) { + // For absolute paths we ensure that it is executable + if strings.Contains(filename, "/") { + err := assertExecutable(filename) + if err != nil { + return nil, fmt.Errorf("absolute path %v is not an executable file: %v", filename, err) + } + return []string{filename}, nil + } + + return p.file.Locate(filename) +} + +// assertExecutable checks whether the specified path is an execuable file. +func assertExecutable(filename string) error { + err := assertFile(filename) + if err != nil { + return err + } + info, err := os.Stat(filename) + if err != nil { + return err + } + + if info.Mode()&0111 == 0 { + return fmt.Errorf("specified file '%v' is not executable", filename) + } + + return nil +}