package config import ( "errors" "io" "os" "path/filepath" "reflect" "strings" ) const maxConfigBytes = 1 << 20 const maxConfigDepth = 64 const maxConfigNodes = 100000 // Decoder must not retain or mutate input and must support concurrent calls // when its Loader is shared. It is trusted application code, not a sandbox. type Decoder interface { Decode([]byte) (Document, error) } type DecoderFunc func([]byte) (Document, error) func (f DecoderFunc) Decode(data []byte) (Document, error) { return f(data) } // Loader owns an immutable per-instance decoder registry. No global registration. type Loader struct { decoders map[string]Decoder sealed bool lookup func(string) (string, bool) } type LoaderOption func(*Loader) error // WithDecoder adds a new extension; builtin/duplicate extensions cannot be replaced. func WithDecoder(extension string, decoder Decoder) LoaderOption { return func(l *Loader) error { if l == nil || l.decoders == nil || l.sealed { return errors.New("config: decoder registry cannot be changed after construction") } ext := strings.ToLower(strings.TrimPrefix(extension, ".")) if ext == "" || strings.ContainsAny(ext, "./\\ \t\r\n") { return errors.New("config: invalid decoder extension") } if decoder == nil { return errors.New("config: nil decoder") } v := reflect.ValueOf(decoder) if (v.Kind() == reflect.Pointer || v.Kind() == reflect.Func || v.Kind() == reflect.Map || v.Kind() == reflect.Slice || v.Kind() == reflect.Interface) && v.IsNil() { return errors.New("config: nil decoder") } if _, exists := l.decoders[ext]; exists { return errors.New("config: decoder extension already registered") } l.decoders[ext] = decoder return nil } } func NewLoader(options ...LoaderOption) (*Loader, error) { l := &Loader{lookup: os.LookupEnv, decoders: map[string]Decoder{"json": JSONDecoder{}, "yaml": YAMLDecoder{}, "yml": YAMLDecoder{}, "xml": XMLDecoder{}}} for _, o := range options { if o == nil { return nil, errors.New("config: nil loader option") } if err := o(l); err != nil { return nil, err } } l.sealed = true return l, nil } var defaultLoader, _ = NewLoader() func selectLoader(loaders []*Loader) (*Loader, error) { if len(loaders) == 0 { return defaultLoader, nil } if len(loaders) != 1 || loaders[0] == nil { return nil, errors.New("config: expected one non-nil loader") } return loaders[0], nil } func (l *Loader) read(path string) (doc Document, err error) { defer func() { if err != nil { var d *Error if errors.As(err, &d) { c := *d c.File = path err = &c } else { reason := "cannot read configuration file" if errors.Is(err, os.ErrNotExist) { reason = "configuration file does not exist" } else if errors.Is(err, os.ErrPermission) { reason = "permission denied reading configuration file" } err = &Error{File: path, Reason: reason, cause: err} } } }() decoder := l.decoders[strings.TrimPrefix(strings.ToLower(filepath.Ext(path)), ".")] if decoder == nil { return doc, &Error{Reason: "unsupported configuration format"} } info, e := os.Stat(path) if e != nil { return doc, e } if !info.Mode().IsRegular() { return doc, &Error{Reason: "configuration must be a regular file"} } f, e := os.Open(path) if e != nil { return doc, e } defer f.Close() info, e = f.Stat() if e != nil { return doc, e } if !info.Mode().IsRegular() { return doc, &Error{Reason: "configuration must be a regular file"} } data, e := io.ReadAll(io.LimitReader(f, maxConfigBytes+1)) if e != nil { return doc, e } if len(data) > maxConfigBytes { return doc, &Error{Reason: "file exceeds 1 MiB"} } doc, err = decodeSafely(decoder, data) if err != nil { return doc, err } if _, ok := doc.Value.(map[string]any); !ok { return doc, doc.diagnostic("", "root must be an object", nil) } return resolveDocument(doc, l.lookup) } func decodeSafely(decoder Decoder, data []byte) (doc Document, err error) { defer func() { if recover() != nil { err = &Error{Reason: "decoder panicked"} } }() doc, err = decoder.Decode(data) if err != nil { var d *Error if !errors.As(err, &d) { err = &Error{Reason: "invalid configuration syntax", cause: err} } } return } // DecodeFile overlays a bounded document onto independent defaults. With no // loader argument it accepts JSON/YAML/XML. It never opens dependencies. func DecodeFile[T any](path string, defaults T, loaders ...*Loader) (T, error) { var zero T l, err := selectLoader(loaders) if err != nil { return zero, err } doc, err := l.read(path) if err != nil { return zero, err } if err := rejectEnvironmentTags(reflect.TypeOf(defaults), map[reflect.Type]bool{}); err != nil { return zero, err } result, err := bindDocument(doc, defaults) if err != nil { return zero, locateError(err, path, doc) } return result, nil }