194 lines
5.6 KiB
Go
194 lines
5.6 KiB
Go
package engine
|
|
|
|
import (
|
|
"errors"
|
|
"io/fs"
|
|
"path"
|
|
"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 := s.access.join(candidate.absolute, name)
|
|
relative := path.Join(candidate.relative, name)
|
|
if !s.access.contains(absolute) {
|
|
return nil, []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "glob traversal is outside the configuration root", &location)}
|
|
}
|
|
canonical, err := s.access.canonical(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 !s.access.contains(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 s.accounting.sources+len(next) > s.limits.sources {
|
|
return nil, []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "glob matches exceed the remaining source-count limit", &location)}
|
|
}
|
|
continue
|
|
}
|
|
info, err := s.access.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 := s.access.canonical(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 !s.access.contains(canonical) {
|
|
return nil, []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "glob directory resolves outside the configuration root", &location)}
|
|
}
|
|
if _, err := s.access.relative(canonical); err != nil {
|
|
return nil, []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "glob directory could not be addressed inside the configuration root", &location)}
|
|
}
|
|
var matches []string
|
|
var problem *Diagnostic
|
|
err = s.access.readDirectory(canonical, func(entry fs.DirEntry) bool {
|
|
s.accounting.entries++
|
|
if s.accounting.entries > s.limits.entries {
|
|
value := importDiagnostic(CodeImportLimitExceeded, "glob traversal exceeds the directory-entry visit limit", &location)
|
|
problem = &value
|
|
return false
|
|
}
|
|
matched, matchErr := path.Match(pattern, entry.Name())
|
|
if matchErr != nil {
|
|
value := importDiagnostic(CodeImportInvalid, "import glob pattern is invalid", &location)
|
|
problem = &value
|
|
return false
|
|
}
|
|
if matched {
|
|
matches = append(matches, entry.Name())
|
|
}
|
|
return true
|
|
})
|
|
if problem != nil {
|
|
return nil, []Diagnostic{*problem}
|
|
}
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, []Diagnostic{globFilesystemDiagnostic(err, "glob directory could not be traversed", 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)
|
|
}
|