Files

107 lines
2.3 KiB
Go

// Copyright (c) 2026 Storm Dragon
// SPDX-License-Identifier: GPL-3.0-or-later
// Package tentaclewire shares bounded framing between the host and Go SDK.
package tentaclewire
import (
"bufio"
"bytes"
"encoding/json"
"errors"
"io"
"unicode/utf8"
)
const MaxFrameBytes = 64 * 1024
var ErrProtocol = errors.New("invalid tentacle protocol message")
var ErrLimit = errors.New("tentacle resource limit exceeded")
// ReadFrame bounds allocation even when the child never writes a newline.
func ReadFrame(reader *bufio.Reader) ([]byte, error) {
var frame []byte
for {
part, err := reader.ReadSlice('\n')
if len(frame)+len(part) > MaxFrameBytes+1 {
return nil, ErrLimit
}
frame = append(frame, part...)
if err == nil {
return frame[:len(frame)-1], nil
}
if errors.Is(err, bufio.ErrBufferFull) {
continue
}
if errors.Is(err, io.EOF) && len(frame) != 0 {
return nil, io.ErrUnexpectedEOF
}
return nil, err
}
}
// Reject duplicate keys and excessive nesting, including in command arguments
// and results. Different languages must not interpret the same frame differently.
func ValidateJSON(data []byte) error {
if !utf8.Valid(data) {
return ErrProtocol
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.UseNumber()
if err := validate_value(decoder, 0); err != nil {
return ErrProtocol
}
if _, err := decoder.Token(); err != io.EOF {
return ErrProtocol
}
return nil
}
func validate_value(decoder *json.Decoder, depth int) error {
if depth > 32 {
return ErrLimit
}
token, err := decoder.Token()
if err != nil {
return err
}
delimiter, composite := token.(json.Delim)
if !composite {
return nil
}
if delimiter != '{' && delimiter != '[' {
return ErrProtocol
}
keys := map[string]bool{}
for decoder.More() {
if delimiter == '{' {
keyToken, err := decoder.Token()
if err != nil {
return err
}
key, ok := keyToken.(string)
if !ok || keys[key] {
return ErrProtocol
}
keys[key] = true
}
if err := validate_value(decoder, depth+1); err != nil {
return err
}
}
_, err = decoder.Token()
return err
}
func DecodeStrict(data []byte, target any) error {
if err := ValidateJSON(data); err != nil {
return err
}
decoder := json.NewDecoder(bytes.NewReader(data))
decoder.DisallowUnknownFields()
if err := decoder.Decode(target); err != nil {
return ErrProtocol
}
return nil
}