package engine import ( "bytes" "errors" "io" "io/fs" "os" "path/filepath" "reflect" "sync/atomic" ) 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 createErr sourceCreationError } type sourceKind uint8 const ( sourceInvalid sourceKind = iota sourceFile sourceFilesystem sourceBytes sourceReader sourceNone ) type sourceCreationError uint8 const ( creationValid sourceCreationError = iota creationInvalidPath 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 } // 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 } // NoSource explicitly selects a defaults-only input. It creates no source record. 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 info *SourceInfo } func readSource(source Source, sourceID string, limits sourceLimits) (sourceRead, []Diagnostic) { if diagnostic := sourceConstructorDiagnostic(source); diagnostic != nil { return sourceRead{}, []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 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 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 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" 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" } 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): 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 } }