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,176 @@
|
||||
// Copyright (c) 2026 Storm Dragon
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
// Package tentacle implements the experimental process host used by explicit
|
||||
// local reader launches. Start and Invoke never wait for child pipe I/O.
|
||||
package tentacle
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.stormux.org/storm/cthulhu/internal/tentaclewire"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
MaxFrameBytes = tentaclewire.MaxFrameBytes // Excludes the mandatory newline.
|
||||
MaxArgumentBytes = 16 * 1024
|
||||
MaxCommands = 32
|
||||
MaxDiagnosticBytes = 64 * 1024 // Lifetime budget; diagnostics are discarded.
|
||||
)
|
||||
|
||||
var (
|
||||
ErrBusy = errors.New("tentacle host capacity exhausted")
|
||||
ErrUnavailable = errors.New("tentacle is not active")
|
||||
ErrDisabled = errors.New("tentacle disabled")
|
||||
ErrProtocol = tentaclewire.ErrProtocol
|
||||
ErrLimit = tentaclewire.ErrLimit
|
||||
ErrExited = errors.New("tentacle process exited")
|
||||
)
|
||||
|
||||
type Version struct {
|
||||
Major int `json:"major"`
|
||||
Minor int `json:"minor"`
|
||||
}
|
||||
|
||||
type Command struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Description string `json:"description"`
|
||||
Bindings []Binding `json:"bindings,omitempty"`
|
||||
Presentation bool `json:"presentation,omitempty"`
|
||||
Interaction bool `json:"interaction,omitempty"`
|
||||
Settings bool `json:"settings,omitempty"` // Opens settings; never a continuation handler.
|
||||
}
|
||||
|
||||
type Initialization struct {
|
||||
Protocol Version `json:"protocol"`
|
||||
Session string `json:"session"`
|
||||
APIs []string `json:"apis"`
|
||||
Capabilities []string `json:"capabilities"`
|
||||
}
|
||||
|
||||
type Registration struct {
|
||||
Protocol Version `json:"protocol"`
|
||||
Session string `json:"session"`
|
||||
Commands []Command `json:"commands"`
|
||||
}
|
||||
|
||||
// RPCError retains the bounded remote message for a future accessible details
|
||||
// view. Error deliberately omits child-supplied text from normal diagnostics.
|
||||
type RPCError struct {
|
||||
Code int `json:"code"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (failure *RPCError) Error() string {
|
||||
return fmt.Sprintf("tentacle RPC error %d", failure.Code)
|
||||
}
|
||||
|
||||
type response struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID string `json:"id"`
|
||||
Result json.RawMessage `json:"result"`
|
||||
Error *RPCError `json:"error"`
|
||||
}
|
||||
|
||||
type request struct {
|
||||
JSONRPC string `json:"jsonrpc"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Method string `json:"method"`
|
||||
Params any `json:"params,omitempty"`
|
||||
}
|
||||
|
||||
func encode_request(id, method string, params any) ([]byte, error) {
|
||||
data, err := json.Marshal(request{JSONRPC: "2.0", ID: id, Method: method, Params: params})
|
||||
if err != nil {
|
||||
return nil, ErrProtocol
|
||||
}
|
||||
if len(data) > MaxFrameBytes {
|
||||
return nil, ErrLimit
|
||||
}
|
||||
return append(data, '\n'), nil
|
||||
}
|
||||
|
||||
func read_frame(reader *bufio.Reader) ([]byte, error) { return tentaclewire.ReadFrame(reader) }
|
||||
func validate_json(data []byte) error { return tentaclewire.ValidateJSON(data) }
|
||||
func decode_strict(data []byte, target any) error { return tentaclewire.DecodeStrict(data, target) }
|
||||
|
||||
func decode_response(data []byte) (response, error) {
|
||||
var reply response
|
||||
if err := decode_strict(data, &reply); err != nil {
|
||||
return reply, err
|
||||
}
|
||||
// Presence matters: a null result is valid; a null error is not.
|
||||
var fields map[string]json.RawMessage
|
||||
_ = json.Unmarshal(data, &fields)
|
||||
for key := range fields {
|
||||
if key != "jsonrpc" && key != "id" && key != "result" && key != "error" {
|
||||
return reply, ErrProtocol
|
||||
}
|
||||
}
|
||||
_, hasResult := fields["result"]
|
||||
_, hasError := fields["error"]
|
||||
if reply.JSONRPC != "2.0" || reply.ID == "" || len(reply.ID) > 32 || hasResult == hasError ||
|
||||
(hasError && (reply.Error == nil || len(reply.Error.Message) > 1024)) {
|
||||
return reply, ErrProtocol
|
||||
}
|
||||
if hasError {
|
||||
var errorFields map[string]json.RawMessage
|
||||
_ = json.Unmarshal(fields["error"], &errorFields)
|
||||
if errorFields["code"] == nil || bytes.Equal(errorFields["code"], []byte("null")) ||
|
||||
errorFields["message"] == nil || bytes.Equal(errorFields["message"], []byte("null")) {
|
||||
return reply, ErrProtocol
|
||||
}
|
||||
}
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
func valid_id(value string) bool {
|
||||
if len(value) == 0 || len(value) > 128 || value[0] < 'a' || value[0] > 'z' {
|
||||
return false
|
||||
}
|
||||
for _, char := range value {
|
||||
if !(char >= 'a' && char <= 'z' || char >= '0' && char <= '9' || char == '-' || char == '.') {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validate_registration(registration Registration, session string) error {
|
||||
if registration.Protocol != (Version{Major: 1, Minor: 0}) || registration.Session != session ||
|
||||
len(registration.Commands) == 0 || len(registration.Commands) > MaxCommands {
|
||||
return ErrProtocol
|
||||
}
|
||||
seen := make(map[string]bool)
|
||||
settings := false
|
||||
for _, command := range registration.Commands {
|
||||
if command.Settings && (!command.Interaction || settings) || command.Interaction && !command.Presentation || !valid_id(command.ID) || seen[command.ID] || strings.TrimSpace(command.Title) == "" ||
|
||||
len(command.Title) > 256 || len(command.Description) > 1024 {
|
||||
return ErrProtocol
|
||||
}
|
||||
seen[command.ID] = true
|
||||
settings = settings || command.Settings
|
||||
if len(command.Bindings) > 4 {
|
||||
return ErrLimit
|
||||
}
|
||||
for _, binding := range command.Bindings {
|
||||
if err := validate_binding(binding); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func clone_commands(commands []Command) []Command {
|
||||
result := append([]Command(nil), commands...)
|
||||
for index := range result {
|
||||
result[index].Bindings = append([]Binding(nil), result[index].Bindings...)
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in new issue
Block a user