106 lines
2.6 KiB
Go
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()
|
|
}
|