// Copyright (c) 2026 Petr BalvĂ­n (https://petrbalvin.org) // SPDX-License-Identifier: MIT package main import ( "errors" "flag" "fmt" "os" "sourcedock.dev/petrbalvin/interpres/v2" ) // A tlsFile is the [tls] table of the configuration file. type tlsFile struct { Cert string `toml:"cert"` Key string `toml:"key"` } // An exportFile is one [[export]] table of the configuration file. type exportFile struct { Path string `toml:"path"` ReadOnly bool `toml:"read-only"` RootSquash bool `toml:"root-squash"` } // A fileConfig mirrors the whole configuration file. Every key is // optional; a key the file leaves out keeps the flag or the built in // default. type fileConfig struct { Listen string `toml:"listen"` LogOps bool `toml:"log-ops"` StateDir string `toml:"state-dir"` MaxConnections int `toml:"max-connections"` TLS *tlsFile `toml:"tls"` Exports []exportFile `toml:"export"` } // loadConfig reads and validates one configuration file. An empty path // reads nothing and answers the zero configuration: the file is never // read unless the operator names it. func loadConfig(path string) (fileConfig, error) { var cfg fileConfig if path == "" { return cfg, nil } data, err := os.ReadFile(path) if err != nil { return cfg, err } if err := interpres.Unmarshal(data, &cfg, interpres.RejectUnknownFields(true)); err != nil { if se, ok := errors.AsType[*interpres.SyntaxError](err); ok { return cfg, fmt.Errorf("%s:%d:%d: %s", path, se.Line, se.Column, se.Msg) } return cfg, fmt.Errorf("%s: %v", path, err) } if len(cfg.Exports) != 1 { return cfg, fmt.Errorf("%s: exactly one [[export]] is required, found %d", path, len(cfg.Exports)) } if cfg.Exports[0].Path == "" { return cfg, fmt.Errorf("%s: [[export]] names no path", path) } return cfg, nil } // applyConfig folds the file configuration under the flags: the file is // the base, every flag the operator passed overrides it. The visited set // is the flags present on the command line, defaults included. func applyConfig(cfg fileConfig, fs *flag.FlagSet, set func(string) bool) { if v := cfg.Listen; v != "" && !set("addr") { _ = fs.Set("addr", v) } if len(cfg.Exports) == 1 && !set("export") { _ = fs.Set("export", cfg.Exports[0].Path) if cfg.Exports[0].ReadOnly && !set("ro") { _ = fs.Set("ro", "true") } if cfg.Exports[0].RootSquash && !set("root-squash") { _ = fs.Set("root-squash", "true") } } if cfg.LogOps && !set("log-ops") { _ = fs.Set("log-ops", "true") } if v := cfg.StateDir; v != "" && !set("state-dir") { _ = fs.Set("state-dir", v) } if v := cfg.MaxConnections; v != 0 && !set("max-connections") { _ = fs.Set("max-connections", fmt.Sprint(v)) } if cfg.TLS != nil && !set("tls-cert") && !set("tls-key") { if cfg.TLS.Cert != "" { _ = fs.Set("tls-cert", cfg.TLS.Cert) } if cfg.TLS.Key != "" { _ = fs.Set("tls-key", cfg.TLS.Key) } } }