storage.go

  1package plugin
  2
  3import (
  4	"encoding/json"
  5	"errors"
  6	"io/fs"
  7	"os"
  8	"path/filepath"
  9	"regexp"
 10	"sort"
 11	"sync"
 12
 13	"github.com/floatpane/matcha/config"
 14	lua "github.com/yuin/gopher-lua"
 15)
 16
 17var validPluginStoreName = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
 18
 19type pluginStore struct {
 20	path string
 21	mu   sync.Mutex
 22	data map[string]string
 23}
 24
 25func newPluginStore(pluginName string) (*pluginStore, error) {
 26	if !validPluginStoreName.MatchString(pluginName) {
 27		return nil, errors.New("invalid plugin name for storage")
 28	}
 29
 30	cfgDir, err := config.GetConfigDir()
 31	if err != nil {
 32		return nil, err
 33	}
 34
 35	dir := filepath.Join(cfgDir, "plugins", pluginName)
 36	if err := os.MkdirAll(dir, 0o700); err != nil {
 37		return nil, err
 38	}
 39
 40	s := &pluginStore{
 41		path: filepath.Join(dir, "data.json"),
 42		data: map[string]string{},
 43	}
 44	if err := s.load(); err != nil {
 45		return nil, err
 46	}
 47	return s, nil
 48}
 49
 50func (s *pluginStore) load() error {
 51	raw, err := os.ReadFile(s.path)
 52	if errors.Is(err, fs.ErrNotExist) {
 53		return nil
 54	}
 55	if err != nil {
 56		return err
 57	}
 58	if err := json.Unmarshal(raw, &s.data); err != nil {
 59		return err
 60	}
 61	if s.data == nil {
 62		s.data = map[string]string{}
 63	}
 64	return nil
 65}
 66
 67func (s *pluginStore) flush() error {
 68	raw, err := json.MarshalIndent(s.data, "", "  ")
 69	if err != nil {
 70		return err
 71	}
 72
 73	tmp, err := os.CreateTemp(filepath.Dir(s.path), ".data-*.json")
 74	if err != nil {
 75		return err
 76	}
 77	tmpPath := tmp.Name()
 78	defer os.Remove(tmpPath)
 79
 80	if _, err := tmp.Write(raw); err != nil {
 81		tmp.Close()
 82		return err
 83	}
 84	if err := os.Chmod(tmpPath, 0o600); err != nil {
 85		tmp.Close()
 86		return err
 87	}
 88	if err := tmp.Close(); err != nil {
 89		return err
 90	}
 91	return os.Rename(tmpPath, s.path)
 92}
 93
 94func (s *pluginStore) Get(k string) (string, bool) {
 95	s.mu.Lock()
 96	defer s.mu.Unlock()
 97
 98	v, ok := s.data[k]
 99	return v, ok
100}
101
102func (s *pluginStore) Set(k, v string) error {
103	s.mu.Lock()
104	defer s.mu.Unlock()
105
106	s.data[k] = v
107	return s.flush()
108}
109
110func (s *pluginStore) Delete(k string) error {
111	s.mu.Lock()
112	defer s.mu.Unlock()
113
114	delete(s.data, k)
115	return s.flush()
116}
117
118// Keys returns the keys currently stored, sorted lexicographically so plugin
119// authors can rely on a stable iteration order across calls.
120func (s *pluginStore) Keys() []string {
121	s.mu.Lock()
122	defer s.mu.Unlock()
123
124	out := make([]string, 0, len(s.data))
125	for k := range s.data {
126		out = append(out, k)
127	}
128	sort.Strings(out)
129	return out
130}
131
132func (m *Manager) currentStore() (*pluginStore, error) {
133	if m.currentPlugin == "" {
134		return nil, nil
135	}
136	if m.stores == nil {
137		m.stores = make(map[string]*pluginStore)
138	}
139	if s, ok := m.stores[m.currentPlugin]; ok {
140		return s, nil
141	}
142
143	s, err := newPluginStore(m.currentPlugin)
144	if err != nil {
145		return nil, err
146	}
147	m.stores[m.currentPlugin] = s
148	return s, nil
149}
150
151func (m *Manager) luaStoreSet(L *lua.LState) int {
152	key := L.CheckString(1)
153	val := L.CheckString(2)
154
155	s, err := m.currentStore()
156	if err != nil {
157		L.RaiseError("store_set: %v", err)
158		return 0
159	}
160	if s == nil {
161		L.RaiseError("store_set: no plugin context")
162		return 0
163	}
164	if err := s.Set(key, val); err != nil {
165		L.RaiseError("store_set: %v", err)
166	}
167	return 0
168}
169
170func (m *Manager) luaStoreGet(L *lua.LState) int {
171	key := L.CheckString(1)
172
173	s, err := m.currentStore()
174	if err != nil {
175		L.RaiseError("store_get: %v", err)
176		return 0
177	}
178	if s == nil {
179		L.Push(lua.LNil)
180		return 1
181	}
182	if v, ok := s.Get(key); ok {
183		L.Push(lua.LString(v))
184	} else {
185		L.Push(lua.LNil)
186	}
187	return 1
188}
189
190func (m *Manager) luaStoreDelete(L *lua.LState) int {
191	key := L.CheckString(1)
192
193	s, err := m.currentStore()
194	if err != nil {
195		L.RaiseError("store_delete: %v", err)
196		return 0
197	}
198	// No plugin context: silently no-op, matching store_get's behavior so
199	// read+remove operations behave the same when called outside a plugin
200	// (e.g. from a non-plugin Lua chunk). store_set still raises so a
201	// missing-context write is surfaced loudly.
202	if s == nil {
203		return 0
204	}
205	if err := s.Delete(key); err != nil {
206		L.RaiseError("store_delete: %v", err)
207	}
208	return 0
209}
210
211func (m *Manager) luaStoreKeys(L *lua.LState) int {
212	s, err := m.currentStore()
213	if err != nil {
214		L.RaiseError("store_keys: %v", err)
215		return 0
216	}
217	if s == nil {
218		L.Push(L.NewTable())
219		return 1
220	}
221
222	t := L.NewTable()
223	for i, key := range s.Keys() {
224		t.RawSetInt(i+1, lua.LString(key))
225	}
226	L.Push(t)
227	return 1
228}