Preserve directory traversal failure phases
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# Audit Remediation Implementation Plan
|
# Audit Remediation Implementation Plan
|
||||||
|
|
||||||
Status: ready for implementation; all stages pending.
|
Status: in progress; completion is recorded per stage below.
|
||||||
Prepared: 2026-10-25.
|
Prepared: 2026-10-25.
|
||||||
Planning baseline: `4a5316b7fc2da57dcced33190cb6499c9a1beb5c`.
|
Planning baseline: `4a5316b7fc2da57dcced33190cb6499c9a1beb5c`.
|
||||||
Audience: a Luna coding agent implementing the stages sequentially.
|
Audience: a Luna coding agent implementing the stages sequentially.
|
||||||
@@ -229,6 +229,12 @@ Finding: **C01**, including its integrated test improvements.
|
|||||||
Owners: `internal/engine/filesystem_access.go`, `import_selection.go`, `glob.go`,
|
Owners: `internal/engine/filesystem_access.go`, `import_selection.go`, `glob.go`,
|
||||||
`native_filesystem_test.go`, and `resource_context_test.go`.
|
`native_filesystem_test.go`, and `resource_context_test.go`.
|
||||||
|
|
||||||
|
Status: **complete** (C01). Prior completed commits: `2736ddf`, `deb9aa5`,
|
||||||
|
`ae3408a`, `ea27f17`, `08a8c25`, `877c497`. Validation passed:
|
||||||
|
`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -run 'Directory|Glob|Filesystem|Resource' -count=1`,
|
||||||
|
`env GOTOOLCHAIN=local GOPROXY=off GOTELEMETRY=off go test ./internal/engine -count=1`,
|
||||||
|
and `git diff --check`.
|
||||||
|
|
||||||
1. Return private directory failure information distinguishing unsuccessful
|
1. Return private directory failure information distinguishing unsuccessful
|
||||||
initial open from enumeration/close failures. Preserve error unwrapping for
|
initial open from enumeration/close failures. Preserve error unwrapping for
|
||||||
classification; do not expose backing error text.
|
classification; do not expose backing error text.
|
||||||
|
|||||||
@@ -141,40 +141,59 @@ func (a *filesystemAccess) open(name string) (fs.File, error) {
|
|||||||
return a.root.Open(relative)
|
return a.root.Open(relative)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type directoryReadError struct {
|
||||||
|
opened bool
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *directoryReadError) Error() string { return "directory could not be read" }
|
||||||
|
func (e *directoryReadError) Unwrap() error { return e.err }
|
||||||
|
|
||||||
|
func missingDirectoryOpen(err error) bool {
|
||||||
|
var failure *directoryReadError
|
||||||
|
return errors.As(err, &failure) && !failure.opened && errors.Is(err, fs.ErrNotExist)
|
||||||
|
}
|
||||||
|
|
||||||
func (a *filesystemAccess) readDirectory(name string, visit func(fs.DirEntry) bool) error {
|
func (a *filesystemAccess) readDirectory(name string, visit func(fs.DirEntry) bool) error {
|
||||||
file, err := a.open(name)
|
file, err := a.open(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return &directoryReadError{err: err}
|
||||||
|
}
|
||||||
|
failure := func(err error) error {
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &directoryReadError{opened: true, err: err}
|
||||||
}
|
}
|
||||||
if reader, ok := file.(fs.ReadDirFile); ok {
|
if reader, ok := file.(fs.ReadDirFile); ok {
|
||||||
for {
|
for {
|
||||||
entries, readErr := reader.ReadDir(globReadBatch)
|
entries, readErr := reader.ReadDir(globReadBatch)
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if !visit(entry) {
|
if !visit(entry) {
|
||||||
return file.Close()
|
return failure(file.Close())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if errors.Is(readErr, io.EOF) {
|
if errors.Is(readErr, io.EOF) {
|
||||||
return file.Close()
|
return failure(file.Close())
|
||||||
}
|
}
|
||||||
if readErr != nil {
|
if readErr != nil {
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
return readErr
|
return failure(readErr)
|
||||||
}
|
}
|
||||||
if len(entries) == 0 {
|
if len(entries) == 0 {
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
return io.ErrNoProgress
|
return failure(io.ErrNoProgress)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := file.Close(); err != nil {
|
if err := file.Close(); err != nil {
|
||||||
return err
|
return failure(err)
|
||||||
}
|
}
|
||||||
// fs.ReadDir provides the standard fallback for implementations whose opened
|
// fs.ReadDir provides the standard fallback for implementations whose opened
|
||||||
// files do not implement fs.ReadDirFile. It may allocate all entries itself.
|
// files do not implement fs.ReadDirFile. It may allocate all entries itself.
|
||||||
entries, err := fs.ReadDir(a.native, name)
|
entries, err := fs.ReadDir(a.native, name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return failure(err)
|
||||||
}
|
}
|
||||||
for _, entry := range entries {
|
for _, entry := range entries {
|
||||||
if !visit(entry) {
|
if !visit(entry) {
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ func readGlobEntries(access *filesystemAccess, directory string, location Locati
|
|||||||
if errors.Is(err, errImportDirectoryEntryLimit) {
|
if errors.Is(err, errImportDirectoryEntryLimit) {
|
||||||
return nil, false, []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "glob traversal exceeds the directory-entry visit limit", &location)}
|
return nil, false, []Diagnostic{importDiagnostic(CodeImportLimitExceeded, "glob traversal exceeds the directory-entry visit limit", &location)}
|
||||||
}
|
}
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if missingDirectoryOpen(err) {
|
||||||
return nil, true, nil
|
return nil, true, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ func importDirectoryEntries(access *filesystemAccess, directory string, site imp
|
|||||||
if errors.Is(err, errImportDirectoryEntryLimit) {
|
if errors.Is(err, errImportDirectoryEntryLimit) {
|
||||||
return nil, false, importSelectionDiagnostic(site, CodeImportLimitExceeded, "directory search exceeds the directory-entry visit limit")
|
return nil, false, importSelectionDiagnostic(site, CodeImportLimitExceeded, "directory search exceeds the directory-entry visit limit")
|
||||||
}
|
}
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if missingDirectoryOpen(err) {
|
||||||
return nil, true, nil
|
return nil, true, nil
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -246,22 +246,36 @@ func TestNativeFilesystemDetectsRepeatedAndCyclicImports(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type observedFilesystem struct {
|
type observedFilesystem struct {
|
||||||
files fstest.MapFS
|
files fstest.MapFS
|
||||||
opened int
|
opened int
|
||||||
closed int
|
closed int
|
||||||
fallback bool
|
fallback bool
|
||||||
failRead string
|
failOpen string
|
||||||
failStat string
|
failRead string
|
||||||
failDir string
|
failStat string
|
||||||
|
failDir string
|
||||||
|
failReadDir string
|
||||||
|
readDirErr error
|
||||||
|
failNoProgress string
|
||||||
|
failClose string
|
||||||
|
closeErr error
|
||||||
|
failFallbackDir string
|
||||||
|
fallbackErr error
|
||||||
|
fallbackBeforeClose bool
|
||||||
|
handles []*observedFile
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *observedFilesystem) Open(name string) (fs.File, error) {
|
func (f *observedFilesystem) Open(name string) (fs.File, error) {
|
||||||
|
if name == f.failOpen {
|
||||||
|
return nil, fs.ErrNotExist
|
||||||
|
}
|
||||||
inner, err := f.files.Open(name)
|
inner, err := f.files.Open(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
f.opened++
|
f.opened++
|
||||||
file := &observedFile{File: inner, owner: f, name: name}
|
file := &observedFile{File: inner, owner: f, name: name}
|
||||||
|
f.handles = append(f.handles, file)
|
||||||
if directory, ok := inner.(fs.ReadDirFile); ok && !f.fallback {
|
if directory, ok := inner.(fs.ReadDirFile); ok && !f.fallback {
|
||||||
return &observedDirectoryFile{observedFile: file, directory: directory}, nil
|
return &observedDirectoryFile{observedFile: file, directory: directory}, nil
|
||||||
}
|
}
|
||||||
@@ -272,20 +286,36 @@ func (f *observedFilesystem) ReadDir(name string) ([]fs.DirEntry, error) {
|
|||||||
if name == f.failDir {
|
if name == f.failDir {
|
||||||
return nil, errors.New("tracking failure canary")
|
return nil, errors.New("tracking failure canary")
|
||||||
}
|
}
|
||||||
return fs.ReadDir(f.files, name)
|
entries, err := fs.ReadDir(f.files, name)
|
||||||
|
if name == f.failFallbackDir {
|
||||||
|
for _, handle := range f.handles {
|
||||||
|
if handle.name == name && !handle.closed {
|
||||||
|
f.fallbackBeforeClose = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entries, f.fallbackErr
|
||||||
|
}
|
||||||
|
return entries, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *observedFilesystem) assertClosed(t *testing.T) {
|
func (f *observedFilesystem) assertClosed(t *testing.T) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if f.opened == 0 || f.opened != f.closed {
|
if f.opened != f.closed || len(f.handles) != f.opened {
|
||||||
t.Fatalf("filesystem opens=%d closes=%d", f.opened, f.closed)
|
t.Fatalf("filesystem opens=%d closes=%d handles=%d", f.opened, f.closed, len(f.handles))
|
||||||
|
}
|
||||||
|
for _, handle := range f.handles {
|
||||||
|
if !handle.closed || handle.closeCalls != 1 {
|
||||||
|
t.Errorf("filesystem handle %q closed=%t close calls=%d", handle.name, handle.closed, handle.closeCalls)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type observedFile struct {
|
type observedFile struct {
|
||||||
fs.File
|
fs.File
|
||||||
owner *observedFilesystem
|
owner *observedFilesystem
|
||||||
name string
|
name string
|
||||||
|
closed bool
|
||||||
|
closeCalls int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *observedFile) Read(data []byte) (int, error) {
|
func (f *observedFile) Read(data []byte) (int, error) {
|
||||||
@@ -304,7 +334,16 @@ func (f *observedFile) Stat() (fs.FileInfo, error) {
|
|||||||
|
|
||||||
func (f *observedFile) Close() error {
|
func (f *observedFile) Close() error {
|
||||||
f.owner.closed++
|
f.owner.closed++
|
||||||
return f.File.Close()
|
f.closeCalls++
|
||||||
|
f.closed = true
|
||||||
|
err := f.File.Close()
|
||||||
|
if f.name == f.owner.failClose {
|
||||||
|
if f.owner.closeErr != nil {
|
||||||
|
return f.owner.closeErr
|
||||||
|
}
|
||||||
|
return fs.ErrNotExist
|
||||||
|
}
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
type observedDirectoryFile struct {
|
type observedDirectoryFile struct {
|
||||||
@@ -316,5 +355,15 @@ func (f *observedDirectoryFile) ReadDir(count int) ([]fs.DirEntry, error) {
|
|||||||
if f.name == f.owner.failDir {
|
if f.name == f.owner.failDir {
|
||||||
return nil, errors.New("tracking failure canary")
|
return nil, errors.New("tracking failure canary")
|
||||||
}
|
}
|
||||||
return f.directory.ReadDir(count)
|
if f.name == f.owner.failNoProgress {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
entries, err := f.directory.ReadDir(count)
|
||||||
|
if f.name == f.owner.failReadDir {
|
||||||
|
if f.owner.readDirErr != nil {
|
||||||
|
return entries, f.owner.readDirErr
|
||||||
|
}
|
||||||
|
return entries, fs.ErrNotExist
|
||||||
|
}
|
||||||
|
return entries, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
)
|
)
|
||||||
@@ -181,3 +182,191 @@ func TestLoadingClosesNativeFilesAcrossPipelineOutcomes(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDirectorySelectorsFailOnPostOpenNotExistErrors(t *testing.T) {
|
||||||
|
const canary = "DIRECTORY_FAILURE_CANARY"
|
||||||
|
readFailure := errors.Join(fs.ErrNotExist, errors.New(canary))
|
||||||
|
closeFailure := errors.Join(fs.ErrNotExist, errors.New(canary))
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
selector string
|
||||||
|
fallback bool
|
||||||
|
readError bool
|
||||||
|
noProgress bool
|
||||||
|
closeError bool
|
||||||
|
fallbackErr bool
|
||||||
|
}{
|
||||||
|
{name: "directory batched read with entries", selector: "directory", readError: true},
|
||||||
|
{name: "directory close", selector: "directory", closeError: true},
|
||||||
|
{name: "directory fallback with entries", selector: "directory", fallback: true, fallbackErr: true},
|
||||||
|
{name: "directory no progress", selector: "directory", noProgress: true},
|
||||||
|
{name: "glob batched read with entries", selector: "glob", readError: true},
|
||||||
|
{name: "glob close", selector: "glob", closeError: true},
|
||||||
|
{name: "glob fallback with entries", selector: "glob", fallback: true, fallbackErr: true},
|
||||||
|
{name: "glob no progress", selector: "glob", noProgress: true},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
main := "configkit:\n imports:\n"
|
||||||
|
if test.selector == "directory" {
|
||||||
|
main += " - dir: [broken, good]\n pattern: '*.yml'\n"
|
||||||
|
} else {
|
||||||
|
main += " - glob: 'broken/*.yml'\n - glob: 'good/*.yml'\n"
|
||||||
|
}
|
||||||
|
filesystem := &observedFilesystem{
|
||||||
|
files: fstest.MapFS{
|
||||||
|
"main.yml": &fstest.MapFile{Data: []byte(main)},
|
||||||
|
"broken": &fstest.MapFile{Mode: fs.ModeDir | 0o700},
|
||||||
|
"broken/bad.yml": &fstest.MapFile{Data: []byte("value: bad\n")},
|
||||||
|
"good": &fstest.MapFile{Mode: fs.ModeDir | 0o700},
|
||||||
|
"good/good.yml": &fstest.MapFile{Data: []byte("value: good\n")},
|
||||||
|
},
|
||||||
|
fallback: test.fallback,
|
||||||
|
failReadDir: "broken",
|
||||||
|
readDirErr: readFailure,
|
||||||
|
failNoProgress: "broken",
|
||||||
|
failClose: "broken",
|
||||||
|
closeErr: closeFailure,
|
||||||
|
failFallbackDir: "broken",
|
||||||
|
fallbackErr: readFailure,
|
||||||
|
}
|
||||||
|
if !test.readError {
|
||||||
|
filesystem.failReadDir = ""
|
||||||
|
}
|
||||||
|
if !test.noProgress {
|
||||||
|
filesystem.failNoProgress = ""
|
||||||
|
}
|
||||||
|
if !test.closeError {
|
||||||
|
filesystem.failClose = ""
|
||||||
|
}
|
||||||
|
if !test.fallbackErr {
|
||||||
|
filesystem.failFallbackDir = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := testLoad[map[string]any](filesystemFile(newNativeNamespace("bundle", filesystem), "main.yml"), Options[map[string]any]{Imports: true})
|
||||||
|
if result != nil {
|
||||||
|
t.Fatalf("load returned a result after directory failure: %#v", result)
|
||||||
|
}
|
||||||
|
var loadError *LoadError
|
||||||
|
if !errors.As(err, &loadError) {
|
||||||
|
t.Fatalf("load error = %#v, want structured LoadError", err)
|
||||||
|
}
|
||||||
|
diagnostics := loadError.Diagnostics()
|
||||||
|
if len(diagnostics) != 1 {
|
||||||
|
t.Fatalf("diagnostics = %#v, want one directory failure", diagnostics)
|
||||||
|
}
|
||||||
|
diagnostic := diagnostics[0]
|
||||||
|
wantCode := CodeImportInvalid
|
||||||
|
if test.selector == "glob" {
|
||||||
|
wantCode = CodeImportRead
|
||||||
|
}
|
||||||
|
if diagnostic.Code != wantCode || diagnostic.Primary == nil || diagnostic.Primary.SourceID != "source-1" {
|
||||||
|
t.Fatalf("directory diagnostic = %#v, want %q at source-1", diagnostic, wantCode)
|
||||||
|
}
|
||||||
|
var rendered strings.Builder
|
||||||
|
if err := WriteDiagnostics(&rendered, diagnostics); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if strings.Contains(err.Error()+diagnostic.Message+rendered.String(), canary) {
|
||||||
|
t.Fatalf("backing error canary escaped: error=%q diagnostic=%#v rendered=%q", err, diagnostic, rendered.String())
|
||||||
|
}
|
||||||
|
if filesystem.fallbackBeforeClose {
|
||||||
|
t.Fatal("fallback directory enumeration began before the opened handle was closed")
|
||||||
|
}
|
||||||
|
filesystem.assertClosed(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadDirectoryPreservesReadErrorOverCloseError(t *testing.T) {
|
||||||
|
readFailure := errors.New("read failure")
|
||||||
|
closeFailure := errors.New("close failure")
|
||||||
|
filesystem := &observedFilesystem{
|
||||||
|
files: fstest.MapFS{
|
||||||
|
"broken": &fstest.MapFile{Mode: fs.ModeDir | 0o700},
|
||||||
|
"broken/entry": &fstest.MapFile{Data: []byte("entry")},
|
||||||
|
},
|
||||||
|
failReadDir: "broken",
|
||||||
|
readDirErr: readFailure,
|
||||||
|
failClose: "broken",
|
||||||
|
closeErr: closeFailure,
|
||||||
|
}
|
||||||
|
access := newNativeAccess(filesystem, ".", &namespaceToken{})
|
||||||
|
visited := 0
|
||||||
|
err := access.readDirectory("broken", func(fs.DirEntry) bool {
|
||||||
|
visited++
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
if !errors.Is(err, readFailure) || errors.Is(err, closeFailure) || visited != 1 {
|
||||||
|
t.Fatalf("directory read = %v, visited %d; want entries plus primary read failure", err, visited)
|
||||||
|
}
|
||||||
|
if missingDirectoryOpen(err) {
|
||||||
|
t.Fatal("post-open read failure was classified as missing-on-open")
|
||||||
|
}
|
||||||
|
filesystem.assertClosed(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadDirectoryVisitStopCloseNotExistIsPostOpen(t *testing.T) {
|
||||||
|
filesystem := &observedFilesystem{
|
||||||
|
files: fstest.MapFS{
|
||||||
|
"broken": &fstest.MapFile{Mode: fs.ModeDir | 0o700},
|
||||||
|
"broken/entry": &fstest.MapFile{Data: []byte("entry")},
|
||||||
|
},
|
||||||
|
failClose: "broken",
|
||||||
|
}
|
||||||
|
access := newNativeAccess(filesystem, ".", &namespaceToken{})
|
||||||
|
err := access.readDirectory("broken", func(fs.DirEntry) bool { return false })
|
||||||
|
if !errors.Is(err, fs.ErrNotExist) || missingDirectoryOpen(err) {
|
||||||
|
t.Fatalf("visit-stop close error = %v, missing-on-open=%t", err, missingDirectoryOpen(err))
|
||||||
|
}
|
||||||
|
filesystem.assertClosed(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDirectorySelectorsKeepMissingRootsOptional(t *testing.T) {
|
||||||
|
for _, selector := range []string{"directory", "glob"} {
|
||||||
|
t.Run(selector, func(t *testing.T) {
|
||||||
|
main := "configkit:\n imports:\n"
|
||||||
|
if selector == "directory" {
|
||||||
|
main += " - dir: [missing, good]\n pattern: '*.yml'\n required: true\n"
|
||||||
|
} else {
|
||||||
|
main += " - glob: 'missing/*.yml'\n - glob: 'good/*.yml'\n required: true\n"
|
||||||
|
}
|
||||||
|
filesystem := &observedFilesystem{files: fstest.MapFS{
|
||||||
|
"main.yml": &fstest.MapFile{Data: []byte(main)},
|
||||||
|
"good": &fstest.MapFile{Mode: fs.ModeDir | 0o700},
|
||||||
|
"good/good.yml": &fstest.MapFile{Data: []byte("value: good\n")},
|
||||||
|
}}
|
||||||
|
result, err := testLoad[map[string]any](filesystemFile(newNativeNamespace("bundle", filesystem), "main.yml"), Options[map[string]any]{Imports: true})
|
||||||
|
if err != nil || result == nil {
|
||||||
|
t.Fatalf("load with missing search root = %#v, %v", result, err)
|
||||||
|
}
|
||||||
|
filesystem.assertClosed(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDirectorySelectorsTreatMissingOnOpenAsOptional(t *testing.T) {
|
||||||
|
for _, selector := range []string{"directory", "glob"} {
|
||||||
|
t.Run(selector, func(t *testing.T) {
|
||||||
|
main := "configkit:\n imports:\n"
|
||||||
|
if selector == "directory" {
|
||||||
|
main += " - dir: [broken, good]\n pattern: '*.yml'\n required: true\n"
|
||||||
|
} else {
|
||||||
|
main += " - glob: 'broken/*.yml'\n - glob: 'good/*.yml'\n required: true\n"
|
||||||
|
}
|
||||||
|
filesystem := &observedFilesystem{
|
||||||
|
files: fstest.MapFS{
|
||||||
|
"main.yml": &fstest.MapFile{Data: []byte(main)},
|
||||||
|
"broken": &fstest.MapFile{Mode: fs.ModeDir | 0o700},
|
||||||
|
"good": &fstest.MapFile{Mode: fs.ModeDir | 0o700},
|
||||||
|
"good/good.yml": &fstest.MapFile{Data: []byte("value: good\n")},
|
||||||
|
},
|
||||||
|
failOpen: "broken",
|
||||||
|
}
|
||||||
|
result, err := testLoad[map[string]any](filesystemFile(newNativeNamespace("bundle", filesystem), "main.yml"), Options[map[string]any]{Imports: true})
|
||||||
|
if err != nil || result == nil {
|
||||||
|
t.Fatalf("load after missing-on-open root = %#v, %v", result, err)
|
||||||
|
}
|
||||||
|
filesystem.assertClosed(t)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user