109 lines
3.4 KiB
Go
109 lines
3.4 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package tentaclewire
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"git.stormux.org/storm/cthulhu/internal/interaction"
|
|
)
|
|
|
|
// Interaction is an opt-in command result. The host runs forms outside the
|
|
// bounded RPC deadline and invokes Next with the submitted values. Poll is a
|
|
// registered command sampled once per second while the form remains open.
|
|
// A polling reply may announce a changed status or finish the form with Done.
|
|
type Interaction struct {
|
|
Presentation
|
|
Form *interaction.Form `json:"form,omitempty"`
|
|
Next string `json:"next,omitempty"`
|
|
Poll string `json:"poll,omitempty"`
|
|
Source string `json:"source,omitempty"` // "focus" or "clipboard", supplied to Next.
|
|
Clipboard *string `json:"clipboard,omitempty"`
|
|
Done bool `json:"done,omitempty"`
|
|
}
|
|
|
|
type Submission struct {
|
|
Activated string `json:"activated,omitempty"`
|
|
Values map[string]interaction.Value `json:"values,omitempty"`
|
|
Cancelled bool `json:"cancelled,omitempty"`
|
|
Item *Item `json:"item,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// Item is a single copied value, without accessible identities or native handles.
|
|
type Item struct {
|
|
Temporary bool `json:"-"` // Host-only ownership of materialized clipboard data.
|
|
Text string `json:"text,omitempty"`
|
|
Path string `json:"path,omitempty"`
|
|
}
|
|
|
|
func DecodeInteraction(data []byte) (Interaction, error) {
|
|
var value Interaction
|
|
var fields map[string]json.RawMessage
|
|
if json.Unmarshal(data, &fields) != nil || fields == nil {
|
|
return value, ErrProtocol
|
|
}
|
|
for _, field := range fields {
|
|
if bytes.Equal(bytes.TrimSpace(field), []byte("null")) {
|
|
return value, ErrProtocol
|
|
}
|
|
}
|
|
if err := DecodeStrict(data, &value); err != nil {
|
|
return value, err
|
|
}
|
|
if value.Speech != "" || value.Braille != "" {
|
|
message, _ := json.Marshal(value.Presentation)
|
|
if _, err := DecodePresentation(message); err != nil {
|
|
return value, err
|
|
}
|
|
} else if value.Uninterruptible {
|
|
return value, ErrProtocol
|
|
}
|
|
if value.Clipboard != nil && (!utf8.ValidString(*value.Clipboard) || len(*value.Clipboard) > MaxPresentationBytes) {
|
|
return value, ErrLimit
|
|
}
|
|
if value.Source != "" && value.Source != "focus" && value.Source != "clipboard" {
|
|
return value, ErrProtocol
|
|
}
|
|
if value.Form != nil {
|
|
for _, element := range value.Form.Elements {
|
|
if !validFormText(element.Label) || !validFormText(element.ID) {
|
|
return value, ErrProtocol
|
|
}
|
|
for _, item := range element.Items {
|
|
if !validFormText(item.Label) || !validFormText(item.ID) {
|
|
return value, ErrProtocol
|
|
}
|
|
}
|
|
}
|
|
if !validFormText(value.Form.Title) {
|
|
return value, ErrProtocol
|
|
}
|
|
if _, err := interaction.NewSession(*value.Form); err != nil {
|
|
return value, ErrProtocol
|
|
}
|
|
if value.Next == "" || value.Done || value.Source != "" {
|
|
return value, ErrProtocol
|
|
}
|
|
} else if value.Poll != "" || value.Next != "" && value.Source == "" || value.Source != "" && value.Next == "" {
|
|
return value, ErrProtocol
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func validFormText(text string) bool {
|
|
if !utf8.ValidString(text) {
|
|
return false
|
|
}
|
|
for _, r := range text {
|
|
if unicode.IsControl(r) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|