feat: add strict TOML configuration validation

This commit is contained in:
2026-08-31 08:22:28 +00:00
parent 49627a9f5a
commit 31a1314470
10 changed files with 1636 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
package config
import (
"bytes"
"fmt"
"os"
"time"
"unicode/utf8"
"github.com/pelletier/go-toml/v2"
)
// ClientOptions controls platform-specific static validation. Production uses
// CheckFilesystem; documentation tests may perform syntax-only validation for
// a non-native target.
type ClientOptions struct {
Platform Platform
CheckFilesystem bool
}
func LoadServer(path string) (*Server, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read server config: %w", err)
}
return DecodeServer(data)
}
func DecodeServer(data []byte) (*Server, error) {
return DecodeServerWithOverrides(data, nil)
}
// DecodeServerWithOverrides applies only explicitly present flag values after
// TOML decoding. Keys come from the single registry in overrides.go.
func DecodeServerWithOverrides(data []byte, overrides map[string]string) (*Server, error) {
var raw = defaultServerFile()
if err := strictDecode(data, &raw); err != nil {
return nil, fmt.Errorf("decode server config: %w", err)
}
if err := applyServerOverrides(&raw, overrides); err != nil {
return nil, err
}
return validateServer(raw)
}
func LoadClient(path string, platform Platform) (*Client, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("read client config: %w", err)
}
return DecodeClient(data, ClientOptions{Platform: platform, CheckFilesystem: true})
}
func DecodeClient(data []byte, options ClientOptions) (*Client, error) {
return DecodeClientWithOverrides(data, options, nil)
}
// DecodeClientWithOverrides applies explicitly present flags after the file.
func DecodeClientWithOverrides(data []byte, options ClientOptions, overrides map[string]string) (*Client, error) {
var raw = defaultClientFile()
if err := strictDecode(data, &raw); err != nil {
return nil, fmt.Errorf("decode client config: %w", err)
}
if err := applyClientOverrides(&raw, overrides); err != nil {
return nil, err
}
return validateClient(raw, options)
}
func strictDecode(data []byte, target any) error {
if !utf8.Valid(data) {
return fmt.Errorf("configuration is not valid UTF-8")
}
decoder := toml.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(target); err != nil {
return err
}
return nil
}
func parseDuration(name, value string, allowZero bool) (time.Duration, error) {
duration, err := time.ParseDuration(value)
if err != nil {
return 0, fmt.Errorf("%s: %w", name, err)
}
if duration < 0 || (!allowZero && duration == 0) {
return 0, fmt.Errorf("%s must be %s", name, map[bool]string{true: "non-negative", false: "positive"}[allowZero])
}
return duration, nil
}