Add recursive directory search with depth limits
This commit is contained in:
@@ -72,7 +72,7 @@ func selectNamedImportCandidates(access *filesystemAccess, basePath string, spec
|
||||
return selection, nil
|
||||
}
|
||||
|
||||
func selectShallowDirectoryImportCandidates(access *filesystemAccess, basePath string, spec importDeclarationSpec, policy resolvedImportPolicy, limits importLimits, accounting *importAccounting) (importSelection, []Diagnostic) {
|
||||
func selectDirectoryImportCandidates(access *filesystemAccess, basePath string, spec importDeclarationSpec, policy resolvedImportPolicy, limits importLimits, accounting *importAccounting) (importSelection, []Diagnostic) {
|
||||
roots := make([]selectedImportCandidate, 0, len(spec.paths))
|
||||
seenRoots := make(map[string]struct{}, len(spec.paths))
|
||||
for index, relative := range spec.paths {
|
||||
@@ -106,35 +106,53 @@ func selectShallowDirectoryImportCandidates(access *filesystemAccess, basePath s
|
||||
if !isDirectory {
|
||||
return importSelection{}, []Diagnostic{*importSelectionDiagnostic(root.site, CodeImportInvalid, "directory search root is not a directory")}
|
||||
}
|
||||
|
||||
entries := make([]fs.DirEntry, 0)
|
||||
var problem *Diagnostic
|
||||
err := access.readDirectory(root.accessPath, func(entry fs.DirEntry) bool {
|
||||
accounting.entries++
|
||||
if accounting.entries > limits.entries {
|
||||
problem = importSelectionDiagnostic(root.site, CodeImportLimitExceeded, "directory search exceeds the directory-entry visit limit")
|
||||
return false
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
return true
|
||||
})
|
||||
if problem != nil {
|
||||
return importSelection{}, []Diagnostic{*problem}
|
||||
entries, missing, diagnostic := importDirectoryEntries(access, root.accessPath, root.site, limits, accounting)
|
||||
if diagnostic != nil {
|
||||
return importSelection{}, []Diagnostic{*diagnostic}
|
||||
}
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
if missing {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return importSelection{}, []Diagnostic{*importSelectionDiagnostic(root.site, CodeImportInvalid, "directory search root could not be read")}
|
||||
}
|
||||
sort.Slice(entries, func(left, right int) bool { return entries[left].Name() < entries[right].Name() })
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || !matchesImportFilename(entry.Name(), policy.pattern) {
|
||||
stack := []importDirectoryFrame{{directory: root, depth: 0, entries: entries}}
|
||||
for len(stack) != 0 {
|
||||
frameIndex := len(stack) - 1
|
||||
frame := &stack[frameIndex]
|
||||
if frame.next >= len(frame.entries) {
|
||||
stack = stack[:frameIndex]
|
||||
continue
|
||||
}
|
||||
accessPath := access.join(root.accessPath, entry.Name())
|
||||
entry := frame.entries[frame.next]
|
||||
frame.next++
|
||||
accessPath := access.join(frame.directory.accessPath, entry.Name())
|
||||
candidate := selectedImportCandidate{
|
||||
path: normalizedImportCandidatePath(access, accessPath), accessPath: accessPath, site: root.site,
|
||||
path: normalizedImportCandidatePath(access, accessPath), accessPath: accessPath, site: frame.directory.site,
|
||||
}
|
||||
isDirectory, entryErr := importDirectoryEntryIsDirectory(entry)
|
||||
if entryErr != nil {
|
||||
return importSelection{}, []Diagnostic{*importSelectionDiagnostic(frame.directory.site, CodeImportInvalid, "directory entry could not be inspected")}
|
||||
}
|
||||
if isDirectory {
|
||||
if !directoryDepthAllows(policy, frame.depth) {
|
||||
continue
|
||||
}
|
||||
childMissing, childIsDirectory, childDiagnostic := validateNamedImportTarget(access, candidate, false, true)
|
||||
if childDiagnostic != nil {
|
||||
return importSelection{}, []Diagnostic{*childDiagnostic}
|
||||
}
|
||||
if childMissing || !childIsDirectory {
|
||||
continue
|
||||
}
|
||||
childEntries, vanished, childDiagnostic := importDirectoryEntries(access, candidate.accessPath, candidate.site, limits, accounting)
|
||||
if childDiagnostic != nil {
|
||||
return importSelection{}, []Diagnostic{*childDiagnostic}
|
||||
}
|
||||
if !vanished {
|
||||
stack = append(stack, importDirectoryFrame{directory: candidate, depth: frame.depth + 1, entries: childEntries})
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !matchesImportFilename(entry.Name(), policy.pattern) {
|
||||
continue
|
||||
}
|
||||
if _, exists := seenCandidates[candidate.path]; exists {
|
||||
continue
|
||||
@@ -165,6 +183,40 @@ func selectShallowDirectoryImportCandidates(access *filesystemAccess, basePath s
|
||||
return selection, nil
|
||||
}
|
||||
|
||||
type importDirectoryFrame struct {
|
||||
directory selectedImportCandidate
|
||||
depth int
|
||||
entries []fs.DirEntry
|
||||
next int
|
||||
}
|
||||
|
||||
func importDirectoryEntries(access *filesystemAccess, directory string, site importFieldSite, limits importLimits, accounting *importAccounting) ([]fs.DirEntry, bool, *Diagnostic) {
|
||||
entries := make([]fs.DirEntry, 0)
|
||||
err := visitBoundedImportDirectory(access, directory, limits.entries, accounting, func(entry fs.DirEntry) error {
|
||||
entries = append(entries, entry)
|
||||
return nil
|
||||
})
|
||||
if errors.Is(err, errImportDirectoryEntryLimit) {
|
||||
return nil, false, importSelectionDiagnostic(site, CodeImportLimitExceeded, "directory search exceeds the directory-entry visit limit")
|
||||
}
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, true, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, false, importSelectionDiagnostic(site, CodeImportInvalid, "directory search root could not be read")
|
||||
}
|
||||
sort.Slice(entries, func(left, right int) bool { return entries[left].Name() < entries[right].Name() })
|
||||
return entries, false, nil
|
||||
}
|
||||
|
||||
func directoryDepthAllows(policy resolvedImportPolicy, currentDepth int) bool {
|
||||
if !policy.recursive {
|
||||
return false
|
||||
}
|
||||
childDepth := currentDepth + 1
|
||||
return !policy.depthSet || policy.depth == 0 || childDepth < policy.depth
|
||||
}
|
||||
|
||||
func normalizedImportCandidatePath(access *filesystemAccess, accessPath string) string {
|
||||
if access.native != nil {
|
||||
return accessPath
|
||||
|
||||
Reference in New Issue
Block a user