Separate directory entry admission from traversal

This commit is contained in:
2026-10-25 13:25:37 +00:00
parent 2c3eb3308b
commit d314a269d5
2 changed files with 100 additions and 63 deletions

View File

@@ -1,6 +1,6 @@
# Consolidation Implementation Plan # Consolidation Implementation Plan
Status: in progress. Stages 1 through 11 are complete; follow Stage 12 through Stage 20 in order. Status: in progress. Stages 1 through 12 are complete; follow Stage 13 through Stage 20 in order.
The target executor is a Luna coding agent working through one end-to-end sprint. The target executor is a Luna coding agent working through one end-to-end sprint.
## Authority, Baseline, And Execution ## Authority, Baseline, And Execution
@@ -508,7 +508,7 @@ The declaration parser retains its validation ordering, authored member sites,
and nil-versus-empty presence behavior. Import-focused tests and and nil-versus-empty presence behavior. Import-focused tests and
`./scripts/check.sh` passed. `./scripts/check.sh` passed.
## Stage 12 — Extract Directory Entry Admission And Descent ## Stage 12 — Extract Directory Entry Admission And Descent (Complete)
**Owner:** engine `import_selection.go`, using existing directory/link helpers. **Owner:** engine `import_selection.go`, using existing directory/link helpers.
@@ -529,6 +529,11 @@ and nil-versus-empty presence behavior. Import-focused tests and
**Finish:** substantial entry/descent policy is separated from stack orchestration **Finish:** substantial entry/descent policy is separated from stack orchestration
without changing the selection algorithm or adding repeated root walks. without changing the selection algorithm or adding repeated root walks.
Progress notes (2026-10-25): extracted entry inspection and file/directory
admission into a result-returning helper. The selector retains root handling,
stack and active-path mutation, candidate union, sorting, exclusions, and the
required-empty decision. Directory-focused tests and `./scripts/check.sh` passed.
## Stage 13 — Extract Imported-Source Admission From Recursion ## Stage 13 — Extract Imported-Source Admission From Recursion
**Owner:** engine `import_acquisition.go` after Stage 10. **Owner:** engine `import_acquisition.go` after Stage 10.

View File

@@ -139,72 +139,23 @@ func selectDirectoryImportCandidates(access *filesystemAccess, basePath string,
} }
entry := frame.entries[frame.next] entry := frame.entries[frame.next]
frame.next++ frame.next++
accessPath := access.join(frame.directory.accessPath, entry.Name()) result, diagnostic := prepareImportDirectoryEntry(access, basePath, *frame, entry, policy, limits, accounting, activeDirectories)
lexicalPath := access.join(frame.directory.path, entry.Name()) if diagnostic != nil {
candidate := selectedImportCandidate{ return importSelection{}, []Diagnostic{*diagnostic}
path: normalizedImportCandidatePath(access, lexicalPath), accessPath: accessPath,
label: relativeImportLabel(access, basePath, lexicalPath), site: frame.directory.site,
} }
if !policy.hidden && strings.HasPrefix(entry.Name(), ".") { switch result.kind {
case importDirectoryEntrySkip:
continue continue
} case importDirectoryEntryFile:
target, entryErr := access.importEntryTarget(candidate.accessPath, entry, policy.follow) if _, exists := seenCandidates[result.candidate.path]; exists {
if entryErr != nil {
if errors.Is(entryErr, fs.ErrNotExist) {
continue continue
} }
return importSelection{}, []Diagnostic{*importDirectoryTargetDiagnostic(frame.directory.site, entryErr)} seenCandidates[result.candidate.path] = struct{}{}
discovered = append(discovered, result.candidate)
case importDirectoryEntryDirectory:
activeDirectories[result.frame.identity] = struct{}{}
stack = append(stack, result.frame)
} }
if target.symlink && !policy.follow {
continue
}
if target.dir {
if !directoryDepthAllows(policy, frame.depth) {
continue
}
if _, exists := activeDirectories[target.identity]; exists {
return importSelection{}, []Diagnostic{*importSelectionDiagnostic(frame.directory.site, CodeImportCycle, "directory traversal cycle detected")}
}
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 {
if access.native == nil {
candidate.accessPath = target.identity
}
activeDirectories[target.identity] = struct{}{}
stack = append(stack, importDirectoryFrame{directory: candidate, depth: frame.depth + 1, entries: childEntries, identity: target.identity})
}
continue
}
if target.identity != "" && access.native == nil {
candidate.accessPath = target.identity
} else {
canonical, canonicalErr := access.canonical(accessPath)
if canonicalErr != nil {
if errors.Is(canonicalErr, fs.ErrNotExist) {
continue
}
return importSelection{}, []Diagnostic{globFilesystemDiagnostic(canonicalErr, "directory search file could not be canonicalized", frame.directory.site.location)}
}
candidate.accessPath = canonical
}
if !matchesImportFilename(entry.Name(), policy.pattern) {
continue
}
if _, exists := seenCandidates[candidate.path]; exists {
continue
}
seenCandidates[candidate.path] = struct{}{}
discovered = append(discovered, candidate)
} }
} }
sort.Slice(discovered, func(left, right int) bool { return discovered[left].path < discovered[right].path }) sort.Slice(discovered, func(left, right int) bool { return discovered[left].path < discovered[right].path })
@@ -229,6 +180,73 @@ func selectDirectoryImportCandidates(access *filesystemAccess, basePath string,
return selection, nil return selection, nil
} }
func prepareImportDirectoryEntry(access *filesystemAccess, basePath string, frame importDirectoryFrame, entry fs.DirEntry, policy resolvedImportPolicy, limits importLimits, accounting *importAccounting, activeDirectories map[string]struct{}) (importDirectoryEntryResult, *Diagnostic) {
accessPath := access.join(frame.directory.accessPath, entry.Name())
lexicalPath := access.join(frame.directory.path, entry.Name())
candidate := selectedImportCandidate{
path: normalizedImportCandidatePath(access, lexicalPath), accessPath: accessPath,
label: relativeImportLabel(access, basePath, lexicalPath), site: frame.directory.site,
}
if !policy.hidden && strings.HasPrefix(entry.Name(), ".") {
return importDirectoryEntryResult{kind: importDirectoryEntrySkip}, nil
}
target, err := access.importEntryTarget(candidate.accessPath, entry, policy.follow)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return importDirectoryEntryResult{kind: importDirectoryEntrySkip}, nil
}
return importDirectoryEntryResult{}, importDirectoryTargetDiagnostic(frame.directory.site, err)
}
if target.symlink && !policy.follow {
return importDirectoryEntryResult{kind: importDirectoryEntrySkip}, nil
}
if target.dir {
if !directoryDepthAllows(policy, frame.depth) {
return importDirectoryEntryResult{kind: importDirectoryEntrySkip}, nil
}
if _, exists := activeDirectories[target.identity]; exists {
return importDirectoryEntryResult{}, importSelectionDiagnostic(frame.directory.site, CodeImportCycle, "directory traversal cycle detected")
}
missing, isDirectory, diagnostic := validateNamedImportTarget(access, candidate, false, true)
if diagnostic != nil {
return importDirectoryEntryResult{}, diagnostic
}
if missing || !isDirectory {
return importDirectoryEntryResult{kind: importDirectoryEntrySkip}, nil
}
entries, vanished, diagnostic := importDirectoryEntries(access, candidate.accessPath, candidate.site, limits, accounting)
if diagnostic != nil {
return importDirectoryEntryResult{}, diagnostic
}
if vanished {
return importDirectoryEntryResult{kind: importDirectoryEntrySkip}, nil
}
if access.native == nil {
candidate.accessPath = target.identity
}
return importDirectoryEntryResult{kind: importDirectoryEntryDirectory, frame: importDirectoryFrame{
directory: candidate, depth: frame.depth + 1, entries: entries, identity: target.identity,
}}, nil
}
if target.identity != "" && access.native == nil {
candidate.accessPath = target.identity
} else {
canonical, err := access.canonical(accessPath)
if err != nil {
if errors.Is(err, fs.ErrNotExist) {
return importDirectoryEntryResult{kind: importDirectoryEntrySkip}, nil
}
diagnostic := globFilesystemDiagnostic(err, "directory search file could not be canonicalized", frame.directory.site.location)
return importDirectoryEntryResult{}, &diagnostic
}
candidate.accessPath = canonical
}
if !matchesImportFilename(entry.Name(), policy.pattern) {
return importDirectoryEntryResult{kind: importDirectoryEntrySkip}, nil
}
return importDirectoryEntryResult{kind: importDirectoryEntryFile, candidate: candidate}, nil
}
type importDirectoryFrame struct { type importDirectoryFrame struct {
directory selectedImportCandidate directory selectedImportCandidate
depth int depth int
@@ -237,6 +255,20 @@ type importDirectoryFrame struct {
identity string identity string
} }
type importDirectoryEntryKind uint8
const (
importDirectoryEntrySkip importDirectoryEntryKind = iota
importDirectoryEntryFile
importDirectoryEntryDirectory
)
type importDirectoryEntryResult struct {
kind importDirectoryEntryKind
candidate selectedImportCandidate
frame importDirectoryFrame
}
func importDirectoryTargetDiagnostic(site importFieldSite, err error) *Diagnostic { func importDirectoryTargetDiagnostic(site importFieldSite, err error) *Diagnostic {
if isImportLinkCycle(err) { if isImportLinkCycle(err) {
return importSelectionDiagnostic(site, CodeImportCycle, "directory traversal cycle detected") return importSelectionDiagnostic(site, CodeImportCycle, "directory traversal cycle detected")