After commit c018f5fd29 Fenrir is being switched over to go. If you do not want to be part of the testing process, please switch to master.

This commit is contained in:
Storm Dragon committed 2026-10-27 20:30:55 -04:00
1 parent c018f5fd29
commit 0e50de4c76
1047 files changed
+58364 -63747

No files matched your search

+147
View File
@@ -0,0 +1,147 @@
// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
package spellcheck
import (
"context"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"sort"
"strings"
"syscall"
"time"
)
func Exceptions_path(home string) string {
return filepath.Join(home, ".local/share/stormux/fenrirscreenreader/spelling/exceptions.json")
}
func load_exceptions(path string) (map[string]string, error) {
words := make(map[string]string)
file, err := os.Open(path)
if errors.Is(err, os.ErrNotExist) {
return words, nil
}
if err != nil {
return nil, err
}
defer file.Close()
data, err := io.ReadAll(io.LimitReader(file, (1<<20)+1))
if err != nil {
return nil, err
}
if len(data) > 1<<20 {
return nil, fmt.Errorf("exceptions file exceeds 1 MiB")
}
var entries []string
if err := json.Unmarshal(data, &entries); err != nil {
return nil, err
}
for _, word := range entries {
if !valid_word(word) {
return nil, fmt.Errorf("invalid exceptions file")
}
words[strings.ToLower(word)] = word
}
return words, nil
}
func exception_list(words map[string]string) []string {
entries := make([]string, 0, len(words))
for _, word := range words {
entries = append(entries, word)
}
sort.Slice(entries, func(i, j int) bool { return strings.ToLower(entries[i]) < strings.ToLower(entries[j]) })
return entries
}
// Each edit reloads the latest list while holding a stable, per-file lock.
// Atomic replacement alone cannot protect against another reader's stale copy.
func update_exceptions(ctx context.Context, path string, operation Operation, word string) (map[string]string, error) {
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return nil, err
}
lock, err := os.OpenFile(path+".lock", os.O_CREATE|os.O_RDWR|syscall.O_NOFOLLOW|syscall.O_NONBLOCK, 0600)
if err != nil {
return nil, err
}
defer lock.Close()
info, err := lock.Stat()
if err != nil {
return nil, err
}
if !info.Mode().IsRegular() || info.Mode().Perm()&0077 != 0 || info.Sys().(*syscall.Stat_t).Uid != uint32(os.Geteuid()) {
return nil, fmt.Errorf("exceptions lock must be private and owned by the current account")
}
waitCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
defer cancel()
for {
if err := waitCtx.Err(); err != nil {
return nil, err
}
err = syscall.Flock(int(lock.Fd()), syscall.LOCK_EX|syscall.LOCK_NB)
if err == nil {
break
}
if !errors.Is(err, syscall.EWOULDBLOCK) {
return nil, err
}
timer := time.NewTimer(10 * time.Millisecond)
select {
case <-waitCtx.Done():
timer.Stop()
return nil, waitCtx.Err()
case <-timer.C:
}
}
words, err := load_exceptions(path)
if err != nil {
return nil, err
}
key := strings.ToLower(word)
if operation == Add {
words[key] = word
} else {
delete(words, key)
}
if err := save_exceptions(path, words); err != nil {
return nil, err
}
return words, nil
}
func save_exceptions(path string, words map[string]string) error {
data, err := json.MarshalIndent(exception_list(words), "", " ")
if err != nil {
return err
}
if len(data) > 1<<20 {
return fmt.Errorf("exceptions file exceeds 1 MiB")
}
dir := filepath.Dir(path)
if err := os.MkdirAll(dir, 0700); err != nil {
return err
}
file, err := os.CreateTemp(dir, ".exceptions-*.json")
if err != nil {
return err
}
defer os.Remove(file.Name())
if _, err := file.Write(append(data, '\n')); err != nil {
file.Close()
return err
}
if err := file.Sync(); err != nil {
file.Close()
return err
}
if err := file.Close(); err != nil {
return err
}
return os.Rename(file.Name(), path)
}