206 lines
6.1 KiB
Go
206 lines
6.1 KiB
Go
package configkit
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
)
|
|
|
|
const globReadBatch = 128
|
|
|
|
type globCandidate struct {
|
|
absolute string
|
|
relative string
|
|
}
|
|
|
|
func (s *importTraversal) expandImportGlob(basePath string, declaration importDeclaration) ([]string, []Diagnostic) {
|
|
location := declaration.location
|
|
segments, valid := validGlobPattern(declaration.glob)
|
|
if !valid {
|
|
return nil, []Diagnostic{importDiagnostic(CodeImportInvalid, "import glob pattern is invalid or uses recursive ** syntax", &location)}
|
|
}
|
|
|
|
candidates := []globCandidate{{absolute: basePath}}
|
|
for index, segment := range segments {
|
|
last := index == len(segments)-1
|
|
literal, patterned := globLiteralSegment(segment)
|
|
var next []globCandidate
|
|
for _, candidate := range candidates {
|
|
var names []string
|
|
if patterned {
|
|
matched, diagnostics := s.readGlobMatches(candidate.absolute, segment, location)
|
|
if len(diagnostics) != 0 {
|
|
return nil, diagnostics
|
|
}
|
|
names = matched
|
|
} else {
|
|
names = []string{literal}
|
|
}
|
|
for _, name := range names {
|
|
absolute := filepath.Join(candidate.absolute, filepath.FromSlash(name))
|
|
relative := path.Join(candidate.relative, name)
|
|
if !containedPath(s.rootPath, filepath.Clean(absolute)) {
|
|
return nil, []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "glob traversal is outside the configuration root", &location)}
|
|
}
|
|
canonical, err := canonicalExistingPath(absolute)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) && !patterned {
|
|
continue
|
|
}
|
|
return nil, []Diagnostic{globFilesystemDiagnostic(err, "glob match could not be canonicalized", location)}
|
|
}
|
|
if !containedPath(s.rootPath, canonical) {
|
|
return nil, []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "glob traversal resolves outside the configuration root", &location)}
|
|
}
|
|
if last {
|
|
next = append(next, globCandidate{absolute: canonical, relative: relative})
|
|
if len(s.sources)+len(next) > s.limits.sources {
|
|
return nil, []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "glob matches exceed the remaining source-count limit", &location)}
|
|
}
|
|
continue
|
|
}
|
|
info, err := os.Stat(canonical)
|
|
if err != nil {
|
|
return nil, []Diagnostic{globFilesystemDiagnostic(err, "glob traversal target could not be inspected", location)}
|
|
}
|
|
if info.IsDir() {
|
|
next = append(next, globCandidate{absolute: canonical, relative: relative})
|
|
}
|
|
}
|
|
}
|
|
candidates = next
|
|
if len(candidates) == 0 {
|
|
return nil, nil
|
|
}
|
|
}
|
|
|
|
matches := make([]string, len(candidates))
|
|
for index, candidate := range candidates {
|
|
matches[index] = candidate.relative
|
|
}
|
|
slices.Sort(matches)
|
|
return matches, nil
|
|
}
|
|
|
|
func (s *importTraversal) readGlobMatches(directory, pattern string, location Location) ([]string, []Diagnostic) {
|
|
canonical, err := canonicalExistingPath(directory)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, nil
|
|
}
|
|
return nil, []Diagnostic{globFilesystemDiagnostic(err, "glob directory could not be canonicalized", location)}
|
|
}
|
|
if !containedPath(s.rootPath, canonical) {
|
|
return nil, []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "glob directory resolves outside the configuration root", &location)}
|
|
}
|
|
relative, err := filepath.Rel(s.rootPath, canonical)
|
|
if err != nil || filepath.IsAbs(relative) || relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) {
|
|
return nil, []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "glob directory could not be addressed inside the configuration root", &location)}
|
|
}
|
|
directoryFile, err := s.root.Open(relative)
|
|
if err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, nil
|
|
}
|
|
return nil, []Diagnostic{globFilesystemDiagnostic(err, "glob directory could not be opened", location)}
|
|
}
|
|
|
|
var matches []string
|
|
for {
|
|
entries, readErr := directoryFile.ReadDir(globReadBatch)
|
|
for _, entry := range entries {
|
|
s.usedEntries++
|
|
if s.usedEntries > s.limits.entries {
|
|
_ = directoryFile.Close()
|
|
return nil, []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "glob traversal exceeds the directory-entry visit limit", &location)}
|
|
}
|
|
matched, matchErr := path.Match(pattern, entry.Name())
|
|
if matchErr != nil {
|
|
_ = directoryFile.Close()
|
|
return nil, []Diagnostic{importDiagnostic(CodeImportInvalid, "import glob pattern is invalid", &location)}
|
|
}
|
|
if matched {
|
|
matches = append(matches, entry.Name())
|
|
}
|
|
}
|
|
if errors.Is(readErr, io.EOF) {
|
|
break
|
|
}
|
|
if readErr != nil {
|
|
_ = directoryFile.Close()
|
|
return nil, []Diagnostic{globFilesystemDiagnostic(readErr, "glob directory could not be read", location)}
|
|
}
|
|
}
|
|
if err := directoryFile.Close(); err != nil {
|
|
return nil, []Diagnostic{globFilesystemDiagnostic(err, "glob directory could not be closed", location)}
|
|
}
|
|
slices.Sort(matches)
|
|
return matches, nil
|
|
}
|
|
|
|
func validGlobPattern(pattern string) ([]string, bool) {
|
|
if !validImportPath(pattern) {
|
|
return nil, false
|
|
}
|
|
segments := strings.Split(pattern, "/")
|
|
for _, segment := range segments {
|
|
if segment == "" || hasRecursiveStars(segment) {
|
|
return nil, false
|
|
}
|
|
if _, err := path.Match(segment, ""); err != nil {
|
|
return nil, false
|
|
}
|
|
}
|
|
return segments, true
|
|
}
|
|
|
|
func hasRecursiveStars(segment string) bool {
|
|
inClass := false
|
|
for index := 0; index < len(segment); index++ {
|
|
switch segment[index] {
|
|
case '\\':
|
|
index++
|
|
case '[':
|
|
inClass = true
|
|
case ']':
|
|
inClass = false
|
|
case '*':
|
|
if !inClass && index+1 < len(segment) && segment[index+1] == '*' {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func globLiteralSegment(segment string) (string, bool) {
|
|
var literal strings.Builder
|
|
for index := 0; index < len(segment); index++ {
|
|
switch segment[index] {
|
|
case '*', '?', '[':
|
|
return "", true
|
|
case '\\':
|
|
index++
|
|
if index < len(segment) {
|
|
literal.WriteByte(segment[index])
|
|
}
|
|
default:
|
|
literal.WriteByte(segment[index])
|
|
}
|
|
}
|
|
return literal.String(), false
|
|
}
|
|
|
|
func globFilesystemDiagnostic(err error, message string, location Location) Diagnostic {
|
|
code := CodeImportRead
|
|
if errors.Is(err, fs.ErrPermission) {
|
|
code = CodeImportPermission
|
|
}
|
|
return importDiagnostic(code, message, &location)
|
|
}
|