Cthulhu is no longer a fork. Now it is a screen reader written in Golang.
This commit is contained in:
1 parent
a998fe67de
commit
069fc674dc
708 files changed
+112482
-532
No files matched your search
@@ -0,0 +1,245 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package tentacle
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"github.com/pelletier/go-toml/v2"
|
||||
)
|
||||
|
||||
// Entry records an explicitly added local program or a verified package.
|
||||
// Discovery never adds entries. Enablement is desired state; failures retain it.
|
||||
type Entry struct {
|
||||
ID string `toml:"id"`
|
||||
Program []string `toml:"program"`
|
||||
Directory string `toml:"directory,omitempty"`
|
||||
Enabled bool `toml:"enabled"`
|
||||
Presentation bool `toml:"presentation"`
|
||||
Interaction bool `toml:"interaction,omitempty"`
|
||||
Repository string `toml:"repository,omitempty"`
|
||||
RepositoryID string `toml:"repository-id,omitempty"`
|
||||
LocalID string `toml:"local-id,omitempty"`
|
||||
Name string `toml:"name,omitempty"`
|
||||
Version string `toml:"package-version,omitempty"`
|
||||
PackageDirectory string `toml:"package-directory,omitempty"`
|
||||
PackageSHA256 string `toml:"package-sha256,omitempty"`
|
||||
RepositoryRevision string `toml:"repository-revision,omitempty"`
|
||||
}
|
||||
|
||||
func (entry Entry) Spec() Spec {
|
||||
return Spec{ID: entry.ID, Program: append([]string(nil), entry.Program...), Directory: entry.Directory, Presentation: entry.Presentation, Interaction: entry.Interaction}
|
||||
}
|
||||
|
||||
type registryDocument struct {
|
||||
Version int `toml:"version"`
|
||||
Entries []Entry `toml:"tentacles"`
|
||||
Repositories []Repository `toml:"repositories,omitempty"`
|
||||
}
|
||||
|
||||
// Registry is owned by the reader lifecycle worker, independently of profiles.
|
||||
// Its optimistic revision prevents overwriting another editor's changes.
|
||||
type Registry struct {
|
||||
Path string
|
||||
original []byte
|
||||
loaded bool
|
||||
repositories []Repository
|
||||
}
|
||||
|
||||
var ErrRegistrySync = errors.New("tentacle registry was replaced but its directory could not be synced")
|
||||
|
||||
func RegistryPath(settingsPath string) string {
|
||||
return filepath.Join(filepath.Dir(settingsPath), "tentacles.toml")
|
||||
}
|
||||
|
||||
func CloneEntries(entries []Entry) []Entry {
|
||||
result := append([]Entry(nil), entries...)
|
||||
for index := range result {
|
||||
result[index].Program = append([]string(nil), result[index].Program...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ValidateEntries(entries []Entry) error {
|
||||
if len(entries) > DefaultLimits().Processes {
|
||||
return ErrLimit
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
packageDirectories := make(map[string]bool)
|
||||
for _, entry := range entries {
|
||||
if err := validate_spec(entry.Spec()); err != nil {
|
||||
return err
|
||||
}
|
||||
if entry.Repository != "" {
|
||||
origin, err := RepositoryURL(entry.Repository)
|
||||
if err != nil || origin != entry.Repository || !metadata_id(entry.RepositoryID) || !metadata_id(entry.LocalID) {
|
||||
return errors.New("invalid installed tentacle source identity")
|
||||
}
|
||||
} else if entry.RepositoryID != "" || entry.LocalID != "" {
|
||||
return errors.New("tentacle source identity requires a repository origin")
|
||||
}
|
||||
if !metadata_text(entry.Name, false) || !metadata_text(entry.Version, false) {
|
||||
return errors.New("invalid installed tentacle display metadata")
|
||||
}
|
||||
if seen[entry.ID] {
|
||||
return errors.New("duplicate tentacle ID")
|
||||
}
|
||||
if entry.PackageDirectory != "" {
|
||||
relativeProgram, err := filepath.Rel(entry.PackageDirectory, entry.Program[0])
|
||||
if entry.Repository == "" || !filepath.IsAbs(entry.PackageDirectory) ||
|
||||
filepath.Clean(entry.PackageDirectory) != entry.PackageDirectory || entry.Directory != entry.PackageDirectory ||
|
||||
!valid_digest(entry.PackageSHA256) || !valid_revision(entry.RepositoryRevision) ||
|
||||
entry.ID != PackageID(Repository{URL: entry.Repository, ID: entry.RepositoryID}, entry.LocalID) ||
|
||||
err != nil || !package_path(filepath.ToSlash(relativeProgram)) || packageDirectories[entry.PackageDirectory] {
|
||||
return errors.New("invalid installed package lock")
|
||||
}
|
||||
packageDirectories[entry.PackageDirectory] = true
|
||||
} else if entry.PackageSHA256 != "" || entry.RepositoryRevision != "" {
|
||||
return errors.New("package lock requires a package directory")
|
||||
}
|
||||
seen[entry.ID] = true
|
||||
}
|
||||
data, err := toml.Marshal(registryDocument{Version: 1, Entries: entries})
|
||||
if err == nil && len(data) > MaxFrameBytes {
|
||||
return ErrLimit
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func read_registry(path string) ([]byte, error) {
|
||||
file, err := os.OpenFile(path, os.O_RDONLY|syscall.O_NONBLOCK|syscall.O_NOFOLLOW, 0)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer file.Close()
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return nil, errors.New("tentacle registry must be a regular file")
|
||||
}
|
||||
data, err := io.ReadAll(io.LimitReader(file, MaxFrameBytes+1))
|
||||
if len(data) > MaxFrameBytes {
|
||||
return nil, ErrLimit
|
||||
}
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (registry *Registry) Load() ([]Entry, error) {
|
||||
registry.loaded = false
|
||||
data, err := read_registry(registry.Path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var document registryDocument
|
||||
if data != nil {
|
||||
decoder := toml.NewDecoder(bytes.NewReader(data)).DisallowUnknownFields()
|
||||
if err := decoder.Decode(&document); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if document.Version != 1 {
|
||||
return nil, errors.New("unsupported tentacle registry version")
|
||||
}
|
||||
if err := ValidateEntries(document.Entries); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ValidateRepositories(document.Repositories); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
registry.original, registry.loaded = data, true
|
||||
registry.repositories = CloneRepositories(document.Repositories)
|
||||
return CloneEntries(document.Entries), nil
|
||||
}
|
||||
|
||||
// Save validates before touching disk, locks cooperating writers, and atomically
|
||||
// replaces an owner-only file. A failed load can never be overwritten.
|
||||
func (registry *Registry) Save(entries []Entry) error {
|
||||
return registry.SaveState(entries, registry.repositories)
|
||||
}
|
||||
|
||||
func (registry *Registry) Repositories() []Repository {
|
||||
return CloneRepositories(registry.repositories)
|
||||
}
|
||||
|
||||
func (registry *Registry) SaveState(entries []Entry, repositories []Repository) error {
|
||||
if !registry.loaded {
|
||||
return errors.New("tentacle registry has not been loaded successfully")
|
||||
}
|
||||
if err := ValidateEntries(entries); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ValidateRepositories(repositories); err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := toml.Marshal(registryDocument{Version: 1, Entries: entries, Repositories: repositories})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(data) > MaxFrameBytes {
|
||||
return ErrLimit
|
||||
}
|
||||
directory := filepath.Dir(registry.Path)
|
||||
if err := os.MkdirAll(directory, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
lock, err := os.OpenFile(registry.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 registry 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 registry is being edited")
|
||||
}
|
||||
defer syscall.Flock(int(lock.Fd()), syscall.LOCK_UN)
|
||||
current, err := read_registry(registry.Path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Equal(current, registry.original) || (current == nil) != (registry.original == nil) {
|
||||
return errors.New("tentacle registry changed outside the reader; restart before editing")
|
||||
}
|
||||
temporary, err := os.CreateTemp(directory, ".tentacles-*.toml")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(temporary.Name())
|
||||
defer temporary.Close()
|
||||
if _, err := temporary.Write(data); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := temporary.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := temporary.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
dir, err := os.Open(directory)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dir.Close()
|
||||
if err := os.Rename(temporary.Name(), registry.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
registry.original = data
|
||||
registry.repositories = CloneRepositories(repositories)
|
||||
if err := dir.Sync(); err != nil {
|
||||
return fmt.Errorf("%w: %w", ErrRegistrySync, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in new issue
Block a user