74 lines
1.9 KiB
Go
74 lines
1.9 KiB
Go
// Copyright (c) 2026 Storm Dragon
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package tentaclewire
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
const MaxPresentationBytes = 8192 // Per channel, measured in UTF-8 bytes.
|
|
|
|
// Presentation is a command result, never an unsolicited announcement. Empty
|
|
// channels are omitted; neither channel supplies positions or routing targets.
|
|
type Presentation struct {
|
|
Speech string `json:"speech,omitempty"`
|
|
Braille string `json:"braille,omitempty"`
|
|
Uninterruptible bool `json:"uninterruptible,omitempty"`
|
|
}
|
|
|
|
func DecodePresentation(data []byte) (Presentation, error) {
|
|
var value Presentation
|
|
if err := DecodeStrict(data, &value); err != nil {
|
|
return value, err
|
|
}
|
|
var fields map[string]json.RawMessage
|
|
if json.Unmarshal(data, &fields) != nil || fields == nil || len(fields) == 0 {
|
|
return value, ErrProtocol
|
|
}
|
|
for _, field := range fields {
|
|
if bytes.Equal(field, []byte("null")) {
|
|
return value, ErrProtocol
|
|
}
|
|
}
|
|
if value.Speech == "" && value.Braille == "" {
|
|
return value, ErrProtocol
|
|
}
|
|
if value.Uninterruptible && value.Speech == "" {
|
|
return value, ErrProtocol
|
|
}
|
|
for _, text := range []string{value.Speech, value.Braille} {
|
|
if len(text) > MaxPresentationBytes {
|
|
return value, ErrLimit
|
|
}
|
|
if !utf8.ValidString(text) {
|
|
return value, ErrProtocol
|
|
}
|
|
for _, char := range text {
|
|
if unicode.IsControl(char) && char != '\n' && char != '\t' {
|
|
return value, ErrProtocol
|
|
}
|
|
}
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
// The experimental optional API is additive to the command-only handshake.
|
|
func ValidAPIs(apis, capabilities []string) bool {
|
|
if len(apis) != len(capabilities) || len(apis) < 1 || len(apis) > 3 || apis[0] != "commands" {
|
|
return false
|
|
}
|
|
if len(apis) >= 2 && apis[1] != "presentation" || len(apis) == 3 && apis[2] != "interaction" {
|
|
return false
|
|
}
|
|
for index := range apis {
|
|
if apis[index] != capabilities[index] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|