package engine import ( "bytes" "errors" "io" "io/fs" "os" "path/filepath" "reflect" "sync/atomic" "gitea.maximumdirect.net/eric/ConfigKit/internal/document" ) const ( maxSourceBytes = 8 << 20 maxTotalBytes = 64 << 20 ) // 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 label string namespace *nativeNamespace data []byte reader *readerSource mapping *document.Node mapIssue *valueIssue mapBytes int64 mapNodes int createErr sourceCreationError search *Search searchApp string searchRequired bool searchIssues []string } type sourceKind uint8 const ( sourceInvalid sourceKind = iota sourceFile sourceFilesystem sourceBytes sourceReader sourceMapping sourceNone sourceSearch ) type sourceCreationError uint8 const ( creationValid sourceCreationError = iota creationInvalidPath creationInvalidLabel creationInvalidReader creationTooLarge creationInvalidFilesystem creationInvalidFilesystemLabel creationInvalidFilesystemPath creationInvalidMappingRoot ) // 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 } // File returns a source that reopens path for every load attempt. func File(path string) Source { if path == "" { return Source{kind: sourceFile, createErr: creationInvalidPath} } return Source{kind: sourceFile, path: path, label: path} } func filesystemFile(namespace *nativeNamespace, name string) Source { 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} } // Bytes snapshots data immediately. Label must be nonempty and safe to display; // constructor problems are reported when the source is consumed. func Bytes(label string, data []byte) Source { source := Source{kind: sourceBytes, label: label} if label == "" { source.createErr = creationInvalidLabel return source } if len(data) > maxSourceBytes { source.createErr = creationTooLarge return source } source.data = bytes.Clone(data) return source } // Reader returns a single-use source. ConfigKit consumes input but never closes it. // Copies of the returned Source share the same single-use claim. func Reader(label string, input io.Reader) Source { source := Source{kind: sourceReader, label: label} if label == "" { source.createErr = creationInvalidLabel return source } if nilReader(input) { source.createErr = creationInvalidReader return source } source.reader = &readerSource{input: input} return source } // Mapping snapshots supported built-in values into an owned document at // construction. Invalid values are retained as safe deferred diagnostics. func Mapping(label string, values map[string]any) Source { source := Source{kind: sourceMapping, label: label} if !validOverrideLabel(label) { source.createErr = creationInvalidLabel return source } if values == nil { source.createErr = creationInvalidMappingRoot return source } builder := valueBuilder{active: make(map[valueVisit]bool)} source.mapping, source.mapIssue = builder.build(values, Path{}, 0) source.mapBytes, source.mapNodes = builder.bytes, builder.nodes return source } // NoSource selects an absent input for defaults and resolution. It creates no // source record and has no main declarations to traverse when imports are enabled. // An active overlay requires explicit filesystem context. func NoSource() Source { return Source{kind: sourceNone} } type sourceLimits struct { perSource int64 remaining int64 } func defaultSourceLimits() sourceLimits { return sourceLimits{perSource: maxSourceBytes, remaining: maxTotalBytes} } type sourceRead struct { data []byte document *document.Node bytes int64 info *SourceInfo } func readSource(source Source, sourceID string, limits sourceLimits) (sourceRead, []Diagnostic) { if diagnostic := sourceConstructorDiagnostic(source); diagnostic != nil { if source.kind == sourceMapping { diagnostic.Primary = &Location{SourceID: sourceID} return sourceRead{info: mappingSourceInfo(source, sourceID)}, []Diagnostic{*diagnostic} } return sourceRead{}, []Diagnostic{*diagnostic} } if source.kind == sourceMapping && source.mapIssue != nil { diagnostic := source.mapIssue.sourceDiagnostic(sourceID) return sourceRead{info: mappingSourceInfo(source, sourceID)}, []Diagnostic{diagnostic} } if source.kind == sourceNone { return sourceRead{}, nil } limit := min(limits.perSource, limits.remaining) if limit < 0 { limit = 0 } info := &SourceInfo{ID: sourceID, Label: source.label, Layer: LayerBase, Kind: sourceSourceKind(source.kind)} var input io.Reader switch source.kind { case sourceFile: absolute, err := filepath.Abs(source.path) if err != nil { return sourceRead{info: info}, []Diagnostic{sourceDiagnostic(CodeSourceRead, sourceID, "source path could not be resolved")} } info.Path = filepath.Clean(absolute) canonical, err := filepath.EvalSymlinks(info.Path) if err != nil { return sourceRead{info: info}, []Diagnostic{classifySourceError(err, sourceID)} } info.Path = canonical data, failure := readRegularFile(canonical, os.Stat, os.Open, limit) if failure != nil { return sourceRead{info: info}, []Diagnostic{sourceReadDiagnostic(failure, sourceID, CodeSourceLimitExceeded, "source exceeds the configured byte limit")} } return sourceRead{data: bytes.Clone(data), info: info}, nil case sourceFilesystem: access := newNativeAccess(source.namespace.filesystem, ".", source.namespace.token) info.Path = source.path info.namespace = source.namespace.token canonical, err := access.canonical(source.path) 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")} } return sourceRead{data: bytes.Clone(data), info: info}, nil case sourceBytes: input = bytes.NewReader(source.data) case sourceReader: if !source.reader.used.CompareAndSwap(false, true) { return sourceRead{info: info}, []Diagnostic{sourceDiagnostic(CodeSourceReaderUsed, sourceID, "reader source has already been consumed")} } input = source.reader.input case sourceMapping: if source.mapBytes > limit || source.mapNodes > maxDocumentNodes { return sourceRead{info: info}, []Diagnostic{sourceDiagnostic(CodeSourceLimitExceeded, sourceID, "mapping source exceeds the configured input limit")} } owned := document.Clone(source.mapping) setDocumentSourceID(owned, sourceID) return sourceRead{document: owned, bytes: source.mapBytes, info: info}, nil default: return sourceRead{info: info}, []Diagnostic{sourceDiagnostic(CodeSourceInvalid, sourceID, "source is invalid")} } data, failure := readBoundedSource(input, limit) if failure != nil { return sourceRead{info: info}, []Diagnostic{sourceReadDiagnostic(failure, sourceID, CodeSourceLimitExceeded, "source exceeds the configured byte limit")} } return sourceRead{data: bytes.Clone(data), info: info}, nil } func mappingSourceInfo(source Source, sourceID string) *SourceInfo { label := source.label if !validOverrideLabel(label) { label = "mapping" } return &SourceInfo{ID: sourceID, Label: label, Layer: LayerBase, Kind: SourceKindMapping} } func sourceConstructorDiagnostic(source Source) *Diagnostic { var message string code := CodeSourceInvalid switch source.createErr { case creationValid: if source.kind != sourceInvalid { return nil } message = "source is invalid" case creationInvalidPath: message = "source path must not be empty" case creationInvalidLabel: message = "source label must not be empty" if source.kind == sourceMapping { message = "mapping source label must be nonempty and safe to display" } case creationInvalidReader: message = "source reader must not be nil" 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" case creationInvalidMappingRoot: message = "mapping source root must be a non-nil mapping" } 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 case sourceMapping: return SourceKindMapping default: return "" } } func setDocumentSourceID(node *document.Node, sourceID string) { if node == nil { return } node.Location.SourceID = sourceID for index := range node.Mapping { entry := &node.Mapping[index] entry.KeyLocation.SourceID = sourceID entry.ValueLocation.SourceID = sourceID setDocumentSourceID(entry.Value, sourceID) } for _, child := range node.Sequence { setDocumentSourceID(child, sourceID) } } func classifySourceError(err error, sourceID string) Diagnostic { switch { case errors.Is(err, fs.ErrNotExist): return sourceDiagnostic(CodeSourceMissing, sourceID, "source does not exist") case errors.Is(err, fs.ErrPermission): return sourceDiagnostic(CodeSourcePermission, sourceID, "source cannot be accessed") default: return sourceDiagnostic(CodeSourceRead, sourceID, "source could not be read") } } func sourceDiagnostic(code Code, sourceID, message string) Diagnostic { diagnostic := Diagnostic{Code: code, Severity: SeverityError, Message: message} if sourceID != "" { diagnostic.Primary = &Location{SourceID: sourceID} } return diagnostic } func nilReader(input io.Reader) bool { if input == nil { return true } value := reflect.ValueOf(input) switch value.Kind() { case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice: return value.IsNil() default: return false } }