146 lines
5.0 KiB
Go
146 lines
5.0 KiB
Go
// Cthulhu screen reader.
|
|
// Copyright (c) 2026 Storm Dragon
|
|
// Authors: Storm Dragon, Angus Kola
|
|
//
|
|
// This program is free software: you can redistribute it and/or modify it
|
|
// under the terms of the GNU General Public License as published by the Free
|
|
// Software Foundation, either version 3 of the License, or (at your option)
|
|
// any later version.
|
|
//
|
|
// This program is distributed in the hope that it will be useful, but WITHOUT
|
|
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
|
// more details.
|
|
//
|
|
// You should have received a copy of the GNU General Public License along with
|
|
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
|
|
|
|
package config
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/pelletier/go-toml/v2"
|
|
)
|
|
|
|
func defaultDocument() writableDocument {
|
|
return writableDocument{
|
|
SchemaVersion: CurrentSchemaVersion,
|
|
ActiveProfile: "default",
|
|
Profiles: map[string]writableProfile{
|
|
"default": profileFromSettings(DefaultSettings()),
|
|
},
|
|
Applications: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
func decodeWritable(contents []byte) (Settings, error) {
|
|
_, settings, err := decodeDocument(contents)
|
|
return settings, err
|
|
}
|
|
|
|
func decodeDocument(contents []byte) (writableDocument, Settings, error) {
|
|
var envelope struct {
|
|
SchemaVersion int `toml:"schema-version"`
|
|
}
|
|
if err := toml.Unmarshal(contents, &envelope); err != nil {
|
|
return writableDocument{}, Settings{}, fmt.Errorf("parse settings: %w", err)
|
|
}
|
|
if envelope.SchemaVersion != CurrentSchemaVersion {
|
|
return writableDocument{}, Settings{}, fmt.Errorf("unsupported settings schema version %d; expected %d", envelope.SchemaVersion, CurrentSchemaVersion)
|
|
}
|
|
var document writableDocument
|
|
decoder := toml.NewDecoder(bytes.NewReader(contents))
|
|
decoder.DisallowUnknownFields()
|
|
if err := decoder.Decode(&document); err != nil {
|
|
var strictError *toml.StrictMissingError
|
|
if errors.As(err, &strictError) {
|
|
return writableDocument{}, Settings{}, fmt.Errorf("parse settings schema %d: %s", CurrentSchemaVersion, strictError.String())
|
|
}
|
|
return writableDocument{}, Settings{}, fmt.Errorf("parse settings schema %d: %w", CurrentSchemaVersion, err)
|
|
}
|
|
settings, err := effectiveSettings(document)
|
|
if err != nil {
|
|
return writableDocument{}, Settings{}, err
|
|
}
|
|
return document, settings, nil
|
|
}
|
|
|
|
func effectiveSettings(document writableDocument) (Settings, error) {
|
|
if strings.TrimSpace(document.ActiveProfile) == "" {
|
|
return Settings{}, errors.New("validate configuration: active-profile is empty")
|
|
}
|
|
profile, exists := document.Profiles[document.ActiveProfile]
|
|
if !exists {
|
|
return Settings{}, fmt.Errorf("validate configuration: active profile %q does not exist", document.ActiveProfile)
|
|
}
|
|
for _, name := range sortedKeys(document.Profiles) {
|
|
candidate := document.Profiles[name]
|
|
if strings.TrimSpace(name) != name || name == "" {
|
|
return Settings{}, fmt.Errorf("validate configuration: invalid profile name %q", name)
|
|
}
|
|
if err := validateCommandValue("profile name", name); err != nil {
|
|
return Settings{}, err
|
|
}
|
|
candidateSettings := DefaultSettings()
|
|
applyProfile(&candidateSettings, candidate)
|
|
if err := validate(candidateSettings); err != nil {
|
|
return Settings{}, fmt.Errorf("profile %q: %w", name, err)
|
|
}
|
|
}
|
|
for _, application := range sortedKeys(document.Applications) {
|
|
profileName := document.Applications[application]
|
|
if strings.TrimSpace(application) != application || application == "" {
|
|
return Settings{}, fmt.Errorf("validate configuration: invalid application identifier %q", application)
|
|
}
|
|
if err := validateCommandValue("application identifier", application); err != nil {
|
|
return Settings{}, err
|
|
}
|
|
if _, exists := document.Profiles[profileName]; !exists {
|
|
return Settings{}, fmt.Errorf("validate configuration: application %q selects missing profile %q", application, profileName)
|
|
}
|
|
}
|
|
settings := DefaultSettings()
|
|
applyProfile(&settings, profile)
|
|
settings.ActiveProfile = document.ActiveProfile
|
|
return settings, nil
|
|
}
|
|
|
|
func sortedKeys[Value any](values map[string]Value) []string {
|
|
keys := make([]string, 0, len(values))
|
|
for key := range values {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|
|
|
|
func encodeWritable(settings Settings) ([]byte, error) {
|
|
document := defaultDocument()
|
|
profileName := settings.ActiveProfile
|
|
if profileName == "" {
|
|
profileName = document.ActiveProfile
|
|
}
|
|
document.ActiveProfile = profileName
|
|
document.Profiles = map[string]writableProfile{profileName: profileFromSettings(settings)}
|
|
return encodeDocument(document)
|
|
}
|
|
|
|
func encodeDocument(document writableDocument) ([]byte, error) {
|
|
if document.SchemaVersion != CurrentSchemaVersion {
|
|
return nil, fmt.Errorf("encode settings schema version %d; expected %d", document.SchemaVersion, CurrentSchemaVersion)
|
|
}
|
|
if _, err := effectiveSettings(document); err != nil {
|
|
return nil, err
|
|
}
|
|
contents, err := toml.Marshal(document)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encode settings: %w", err)
|
|
}
|
|
return contents, nil
|
|
}
|