Files

106 lines
2.6 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package tentacle
import (
"bytes"
"encoding/json"
"errors"
"os"
"path/filepath"
"syscall"
"git.stormux.org/storm/cthulhu/internal/interaction"
)
// PageValues contains only host-validated declarative preferences. Active speech
// overrides and quick-setting selections are deliberately never restored here.
type PageValues map[string]map[string]map[string]interaction.Value
type PageStore struct {
Path string
original []byte
loaded bool
}
func (store *PageStore) Load() (PageValues, error) {
store.loaded = false
data, err := read_registry(store.Path)
if err != nil {
return nil, err
}
values := PageValues{}
if data != nil {
if validate_json(data) != nil {
return nil, ErrProtocol
}
if err := json.Unmarshal(data, &values); err != nil || values == nil {
return nil, ErrProtocol
}
}
store.original, store.loaded = data, true
return values, nil
}
func (store *PageStore) Save(values PageValues) error {
if !store.loaded {
return errors.New("tentacle preferences were not loaded successfully")
}
data, err := json.Marshal(values)
if err != nil {
return err
}
if len(data) > MaxFrameBytes {
return ErrLimit
}
directory := filepath.Dir(store.Path)
if err := os.MkdirAll(directory, 0700); err != nil {
return err
}
lock, err := os.OpenFile(store.Path+".lock", os.O_CREATE|os.O_RDWR|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0600)
if err != nil {
return err
}
defer lock.Close()
if info, err := lock.Stat(); err != nil || !info.Mode().IsRegular() {
return errors.New("tentacle preferences lock must be a regular file")
}
if err := syscall.Flock(int(lock.Fd()), syscall.LOCK_EX|syscall.LOCK_NB); err != nil {
return errors.New("tentacle preferences are being edited")
}
defer syscall.Flock(int(lock.Fd()), syscall.LOCK_UN)
current, err := read_registry(store.Path)
if err != nil {
return err
}
if !bytes.Equal(current, store.original) || (current == nil) != (store.original == nil) {
return errors.New("tentacle preferences changed outside the reader")
}
file, err := os.CreateTemp(directory, ".tentacle-preferences-*")
if err != nil {
return err
}
defer os.Remove(file.Name())
defer file.Close()
if _, err := file.Write(data); err != nil {
return err
}
if err := file.Sync(); err != nil {
return err
}
if err := file.Close(); err != nil {
return err
}
dir, err := os.Open(directory)
if err != nil {
return err
}
defer dir.Close()
if err := os.Rename(file.Name(), store.Path); err != nil {
return err
}
store.original = data
return dir.Sync()
}