Expose native filesystem sources through public loading API

This commit is contained in:
2026-09-29 17:36:20 +00:00
parent 6111cb23a1
commit 1ff3c812a7
18 changed files with 430 additions and 36 deletions

View File

@@ -1,12 +1,21 @@
package engine
import (
"io/fs"
"os"
"path"
"path/filepath"
"strings"
)
func validateFilesystemContext(source Source, context *FilesystemContext, main parsedSource) *Diagnostic {
if nativeContext(source, context) {
access, basePath, mainPath, diagnostic := nativeFilesystemAccess(source, context)
if diagnostic != nil {
return diagnostic
}
return validateNativeDirectories(access, basePath, mainPath)
}
rootPath, _, _, diagnostic := importFilesystem(source, context, main)
if diagnostic != nil {
return diagnostic
@@ -29,20 +38,87 @@ func filesystemOptionDiagnostics(source Source, context *FilesystemContext) []Di
case sourceNone:
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "NoSource cannot use filesystem context")}
case sourceFile:
if context.FS != nil {
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "host file sources cannot use a native filesystem context")}
}
if context.BaseDir != "" {
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "file sources determine their own import base directory")}
}
case sourceFilesystem:
if context.FS == nil || context.FS.namespace == nil || source.namespace == nil || context.FS.namespace.token != source.namespace.token {
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "filesystem file context must use the source namespace")}
}
if context.BaseDir != "" {
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "file sources determine their own import base directory")}
}
if context.Root != "" && !fs.ValidPath(context.Root) {
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "filesystem root must be a slash-relative valid path")}
}
case sourceBytes, sourceReader:
if context.BaseDir == "" || context.Root == "" {
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "byte and reader filesystem context requires base and root directories")}
}
if !filepath.IsAbs(context.BaseDir) || !filepath.IsAbs(context.Root) {
if context.FS != nil {
if context.FS.namespace == nil || !fs.ValidPath(context.BaseDir) || !fs.ValidPath(context.Root) {
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "filesystem base and root must be slash-relative valid paths")}
}
} else if !filepath.IsAbs(context.BaseDir) || !filepath.IsAbs(context.Root) {
return []Diagnostic{optionDiagnostic(CodeOptionsInvalid, "byte and reader filesystem base and root directories must be absolute")}
}
}
return nil
}
func nativeContext(source Source, context *FilesystemContext) bool {
return source.kind == sourceFilesystem || context != nil && context.FS != nil
}
// nativeFilesystemAccess resolves namespace context without opening caller
// resources. File inputs always derive their base from the selected file.
func nativeFilesystemAccess(source Source, context *FilesystemContext) (*filesystemAccess, string, string, *Diagnostic) {
var namespace *nativeNamespace
rootPath := "."
basePath := "."
mainPath := ""
if source.kind == sourceFilesystem {
namespace = source.namespace
mainPath = source.path
basePath = path.Dir(source.path)
if context != nil && context.Root != "" {
rootPath = context.Root
}
} else if context != nil && context.FS != nil {
namespace = context.FS.namespace
rootPath, basePath = context.Root, context.BaseDir
}
if namespace == nil || nilFilesystem(namespace.filesystem) || !fs.ValidPath(rootPath) || !fs.ValidPath(basePath) {
value := optionDiagnostic(CodeOptionsInvalid, "filesystem context is invalid")
return nil, "", "", &value
}
access := newNativeAccess(namespace.filesystem, rootPath, namespace.token)
access.label = namespace.label
if !access.contains(basePath) || mainPath != "" && !access.contains(mainPath) {
value := importDiagnostic(CodeImportOutsideRoot, "selected source and import base must stay inside the configuration root", nil)
return nil, "", "", &value
}
return access, basePath, mainPath, nil
}
func validateNativeDirectories(access *filesystemAccess, basePath, mainPath string) *Diagnostic {
if !access.contains(basePath) || mainPath != "" && !access.contains(mainPath) {
value := importDiagnostic(CodeImportOutsideRoot, "selected source and import base must stay inside the configuration root", nil)
return &value
}
for _, directory := range []string{access.rootPath, basePath} {
info, err := access.stat(directory)
if err != nil || !info.IsDir() {
value := importDiagnostic(CodeImportInvalid, "configuration root and import base must be directories", nil)
return &value
}
}
return nil
}
func importFilesystem(source Source, context *FilesystemContext, main parsedSource) (rootPath, basePath, mainPath string, diagnostic *Diagnostic) {
if main.source == nil || main.document == nil {
value := importDiagnostic(CodeImportInvalid, "imports require a selected configuration source", nil)

View File

@@ -37,6 +37,7 @@ type filesystemAccess struct {
root *os.Root
native fs.FS
token *namespaceToken
label string
}
func openHostAccess(rootPath string) (*filesystemAccess, error) {

View File

@@ -58,9 +58,12 @@ type importTraversal struct {
}
func loadImportedDocuments(source Source, context *FilesystemContext, main parsedSource, rule *VersionRule, limits importLimits) (*composedDocument, []SourceInfo, []Diagnostic) {
if source.kind == sourceFilesystem {
access := newNativeAccess(source.namespace.filesystem, ".", source.namespace.token)
return loadImportedWithAccess(access, access.dir(source.path), source.path, main, rule, limits, &importAccounting{})
if nativeContext(source, context) {
access, basePath, mainPath, diagnostic := nativeFilesystemAccess(source, context)
if diagnostic != nil {
return nil, sourceInfos(main.source), []Diagnostic{*diagnostic}
}
return loadImportedWithAccess(access, basePath, mainPath, main, rule, limits, &importAccounting{})
}
rootPath, basePath, mainPath, diagnostic := importFilesystem(source, context, main)
if diagnostic != nil {
@@ -231,7 +234,13 @@ func (s *importTraversal) visitPath(basePath string, declaration importDeclarati
return []Diagnostic{importDiagnostic(CodeImportOutsideRoot, "import target could not be addressed inside the configuration root", &location)}
}
sourceID := "source-" + strconv.Itoa(s.accounting.sources+1)
info := SourceInfo{ID: sourceID, Label: declaration.path, Path: canonical, Layer: LayerBase, ImportedFrom: &location}
kind := SourceKindFile
label := declaration.path
if s.access.native != nil {
kind = SourceKindFilesystem
label = s.access.label + "/" + canonical
}
info := SourceInfo{ID: sourceID, Label: label, Path: canonical, Layer: LayerBase, Kind: kind, ImportedFrom: &location}
s.sources = append(s.sources, info)
data, skipped, diagnostic := s.readImport(canonical, sourceID, declaration.optional)
if diagnostic != nil {

View File

@@ -33,6 +33,7 @@ type Options[T any] struct {
type FilesystemContext struct {
BaseDir string
Root string
FS *Filesystem
}
// Override describes one explicitly supplied application override. Value uses

View File

@@ -44,7 +44,7 @@ func prepareOverrides(base *composedDocument, sources []SourceInfo, overrides []
allSources := cloneSourceInfos(sources)
for index, override := range overrides {
sourceID := "source-" + strconv.Itoa(len(sources)+index+1)
info := SourceInfo{ID: sourceID, Label: override.Flag, Layer: LayerCLI}
info := SourceInfo{ID: sourceID, Label: override.Flag, Layer: LayerCLI, Kind: SourceKindCLI}
allSources = append(allSources, info)
prepared[index].source = info
}

View File

@@ -11,6 +11,18 @@ const (
LayerResolution Layer = "resolution"
)
// SourceKind identifies source acquisition independently of precedence.
type SourceKind string
const (
SourceKindFile SourceKind = "file"
SourceKindFilesystem SourceKind = "filesystem"
SourceKindBytes SourceKind = "bytes"
SourceKindReader SourceKind = "reader"
SourceKindMapping SourceKind = "mapping"
SourceKindCLI SourceKind = "cli"
)
// ChangeKind identifies a recorded operation. The zero value is
// used for input and default origins.
type ChangeKind string
@@ -156,14 +168,16 @@ type Location struct {
Column int
}
// SourceInfo describes a source discovered during a load. Path is a canonical
// local path when one exists. ImportedFrom identifies the declaration that
// included this source.
// SourceInfo describes a source discovered during a load. Kind identifies
// acquisition independently of Layer. Path is a canonical host path or a
// namespace-relative filesystem path; synthetic sources have no Path.
// ImportedFrom identifies the declaration that included this source.
type SourceInfo struct {
ID string
Label string
Path string
Layer Layer
Kind SourceKind
ImportedFrom *Location
}

View File

@@ -38,8 +38,14 @@ func loadSelectedOverlayDocument(source Source, context *FilesystemContext, main
}
func loadSelectedOverlayInto(source Source, context *FilesystemContext, main parsedSource, base *assemblyAccumulator, sources []SourceInfo, name string, rule *VersionRule, limits importLimits) ([]SourceInfo, []Diagnostic) {
if source.kind == sourceFilesystem {
access := newNativeAccess(source.namespace.filesystem, ".", source.namespace.token)
if nativeContext(source, context) {
access, basePath, mainPath, diagnostic := nativeFilesystemAccess(source, context)
if diagnostic != nil {
return cloneSourceInfos(sources), []Diagnostic{*diagnostic}
}
if diagnostic := validateNativeDirectories(access, basePath, mainPath); diagnostic != nil {
return cloneSourceInfos(sources), []Diagnostic{*diagnostic}
}
return loadSelectedOverlayWithAccess(access, base, sources, name, rule, limits)
}
rootPath, _, _, diagnostic := importFilesystem(source, context, main)
@@ -92,7 +98,13 @@ func loadSelectedOverlayWithAccess(access *filesystemAccess, base *assemblyAccum
sourceID := "source-" + strconv.Itoa(len(sources)+1)
overlayLocation := &Location{SourceID: sourceID}
overlaySource := SourceInfo{ID: sourceID, Label: "overlays/" + name + ".yml", Path: canonical, Layer: LayerOverlay}
kind := SourceKindFile
label := "overlays/" + name + ".yml"
if access.native != nil {
kind = SourceKindFilesystem
label = access.label + "/" + canonical
}
overlaySource := SourceInfo{ID: sourceID, Label: label, Path: canonical, Layer: LayerOverlay, Kind: kind}
sources = append(cloneSourceInfos(sources), overlaySource)
data, diagnostic := readOverlayFile(access, canonical, sourceID, limits.bytes-base.current.inputBytes)
if diagnostic != nil {

View File

@@ -16,8 +16,8 @@ const (
maxTotalBytes = 64 << 20
)
// Source is an opaque configuration input. Construct sources with File, Bytes,
// Reader, or NoSource; the zero value is invalid.
// Source is an opaque configuration input. Construct sources with File,
// Filesystem.File, Bytes, Reader, or NoSource; the zero value is invalid.
type Source struct {
kind sourceKind
path string
@@ -47,8 +47,52 @@ const (
creationInvalidLabel
creationInvalidReader
creationTooLarge
creationInvalidFilesystem
creationInvalidFilesystemLabel
creationInvalidFilesystemPath
)
// Filesystem is a reusable opaque namespace for caller-owned fs.FS inputs.
// Copies preserve the namespace identity used for explicit contexts.
type Filesystem struct {
namespace *nativeNamespace
createErr sourceCreationError
}
// NewFilesystem records a borrowed filesystem without opening it. Invalid
// arguments are reported when a File source is consumed.
func NewFilesystem(label string, filesystem fs.FS) Filesystem {
result := Filesystem{}
if !validOverrideLabel(label) {
result.createErr = creationInvalidFilesystemLabel
} else if nilFilesystem(filesystem) {
result.createErr = creationInvalidFilesystem
} else {
result.namespace = newNativeNamespace(label, filesystem)
}
return result
}
// File selects a non-root slash-relative file within this namespace.
func (f Filesystem) File(name string) Source {
if f.createErr != creationValid {
return Source{kind: sourceFilesystem, createErr: f.createErr}
}
return filesystemFile(f.namespace, name)
}
func nilFilesystem(filesystem fs.FS) bool {
if filesystem == nil {
return true
}
value := reflect.ValueOf(filesystem)
switch value.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
return value.IsNil()
}
return false
}
type readerSource struct {
input io.Reader
used atomic.Bool
@@ -63,8 +107,11 @@ func File(path string) Source {
}
func filesystemFile(namespace *nativeNamespace, name string) Source {
if namespace == nil || namespace.filesystem == nil || name == "." || !fs.ValidPath(name) {
return Source{kind: sourceFilesystem, createErr: creationInvalidPath}
if namespace == nil || nilFilesystem(namespace.filesystem) {
return Source{kind: sourceFilesystem, createErr: creationInvalidFilesystem}
}
if name == "." || !fs.ValidPath(name) {
return Source{kind: sourceFilesystem, createErr: creationInvalidFilesystemPath}
}
return Source{kind: sourceFilesystem, path: name, label: namespace.label + "/" + name, namespace: namespace}
}
@@ -132,7 +179,7 @@ func readSource(source Source, sourceID string, limits sourceLimits) (sourceRead
if limit < 0 {
limit = 0
}
info := &SourceInfo{ID: sourceID, Label: source.label, Layer: LayerBase}
info := &SourceInfo{ID: sourceID, Label: source.label, Layer: LayerBase, Kind: sourceSourceKind(source.kind)}
var input io.Reader
switch source.kind {
@@ -159,6 +206,7 @@ func readSource(source Source, sourceID string, limits sourceLimits) (sourceRead
if err != nil {
return sourceRead{info: info}, []Diagnostic{classifySourceError(err, sourceID)}
}
info.Path = canonical
data, failure := readRegularFile(canonical, access.stat, access.open, limit)
if failure != nil {
return sourceRead{info: info}, []Diagnostic{sourceReadDiagnostic(failure, sourceID, CodeSourceLimitExceeded, "source exceeds the configured byte limit")}
@@ -199,11 +247,32 @@ func sourceConstructorDiagnostic(source Source) *Diagnostic {
case creationTooLarge:
code = CodeSourceLimitExceeded
message = "source exceeds the configured byte limit"
case creationInvalidFilesystem:
message = "filesystem must not be nil"
case creationInvalidFilesystemLabel:
message = "filesystem label must be nonempty and safe to display"
case creationInvalidFilesystemPath:
message = "filesystem source path must be a non-root slash-relative valid path"
}
diagnostic := sourceDiagnostic(code, "", message)
return &diagnostic
}
func sourceSourceKind(kind sourceKind) SourceKind {
switch kind {
case sourceFile:
return SourceKindFile
case sourceFilesystem:
return SourceKindFilesystem
case sourceBytes:
return SourceKindBytes
case sourceReader:
return SourceKindReader
default:
return ""
}
}
func classifySourceError(err error, sourceID string) Diagnostic {
switch {
case errors.Is(err, fs.ErrNotExist):