Move facade to the root package
This commit is contained in:
211
source.go
211
source.go
@@ -1,216 +1,25 @@
|
||||
package configkit
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
const (
|
||||
maxSourceBytes = 8 << 20
|
||||
maxTotalBytes = 64 << 20
|
||||
"gitea.maximumdirect.net/eric/ConfigKit/internal/engine"
|
||||
)
|
||||
|
||||
// Source is an opaque configuration input. Construct sources with File, Bytes,
|
||||
// Reader, or NoSource; the zero value is invalid.
|
||||
type Source struct {
|
||||
kind sourceKind
|
||||
path string
|
||||
label string
|
||||
data []byte
|
||||
reader *readerSource
|
||||
createErr sourceCreationError
|
||||
}
|
||||
|
||||
type sourceKind uint8
|
||||
|
||||
const (
|
||||
sourceInvalid sourceKind = iota
|
||||
sourceFile
|
||||
sourceBytes
|
||||
sourceReader
|
||||
sourceNone
|
||||
)
|
||||
|
||||
type sourceCreationError uint8
|
||||
|
||||
const (
|
||||
creationValid sourceCreationError = iota
|
||||
creationInvalidPath
|
||||
creationInvalidLabel
|
||||
creationInvalidReader
|
||||
creationTooLarge
|
||||
)
|
||||
|
||||
type readerSource struct {
|
||||
input io.Reader
|
||||
used atomic.Bool
|
||||
}
|
||||
type Source struct{ value engine.Source }
|
||||
|
||||
// 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 File(path string) Source { return Source{value: engine.File(path)} }
|
||||
|
||||
// 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
|
||||
}
|
||||
// Bytes snapshots data immediately.
|
||||
func Bytes(label string, data []byte) Source { return Source{value: engine.Bytes(label, data)} }
|
||||
|
||||
// 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
|
||||
}
|
||||
// Reader returns a single-use source. Copies share the same single-use claim.
|
||||
func Reader(label string, input io.Reader) Source { return Source{value: engine.Reader(label, input)} }
|
||||
|
||||
// NoSource explicitly selects a defaults-only input. It creates no source record.
|
||||
func NoSource() Source {
|
||||
return Source{kind: sourceNone}
|
||||
}
|
||||
// NoSource explicitly selects defaults-only input.
|
||||
func NoSource() Source { return Source{value: engine.NoSource()} }
|
||||
|
||||
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}
|
||||
|
||||
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 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"
|
||||
}
|
||||
diagnostic := sourceDiagnostic(code, "", message)
|
||||
return &diagnostic
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
func (s Source) toEngine() engine.Source { return s.value }
|
||||
|
||||
Reference in New Issue
Block a user