Separate directory entry admission from traversal
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user