+94
-6
@@ -3,16 +3,31 @@ package tgapi
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
maximumRichJSONDepth = 64
|
||||
maximumRichJSONNodes = 10_000
|
||||
)
|
||||
|
||||
// UnmarshalRichText parses a RichText tree from JSON: a string, an array, or
|
||||
// a typed object. Unknown object types that carry a text field are preserved
|
||||
// as RichTextWrap so their nested text remains usable; unmodeled fields are
|
||||
// discarded.
|
||||
// discarded. The fallback representation is subject to change in v2 so unknown
|
||||
// fields can be preserved losslessly.
|
||||
//
|
||||
// Since: Bot API 10.1
|
||||
func UnmarshalRichText(data []byte) (RichText, error) {
|
||||
if err := validateRichJSON(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return unmarshalRichText(data)
|
||||
}
|
||||
|
||||
func unmarshalRichText(data []byte) (RichText, error) {
|
||||
if bytes.Equal(bytes.TrimSpace(data), []byte("null")) {
|
||||
return nil, fmt.Errorf("richtext: null is not a rich text value")
|
||||
}
|
||||
@@ -26,7 +41,7 @@ func UnmarshalRichText(data []byte) (RichText, error) {
|
||||
if err := json.Unmarshal(data, &raw); err == nil {
|
||||
arr := make(RichTextArray, len(raw))
|
||||
for i, it := range raw {
|
||||
rt, err := UnmarshalRichText(it)
|
||||
rt, err := unmarshalRichText(it)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -47,7 +62,7 @@ func UnmarshalRichText(data []byte) (RichText, error) {
|
||||
var inner RichText
|
||||
if len(head.Text) > 0 {
|
||||
var err error
|
||||
if inner, err = UnmarshalRichText(head.Text); err != nil {
|
||||
if inner, err = unmarshalRichText(head.Text); err != nil {
|
||||
return nil, fmt.Errorf("richtext %q: bad text: %w", head.Type, err)
|
||||
}
|
||||
}
|
||||
@@ -202,10 +217,18 @@ func UnmarshalRichText(data []byte) (RichText, error) {
|
||||
|
||||
// UnmarshalRichBlock parses a single RichBlock from JSON, dispatching on the
|
||||
// type tag. Unknown types that carry a text field are decoded as RichBlockWrap
|
||||
// so their nested text remains usable; unmodeled fields are discarded.
|
||||
// so their nested text remains usable; unmodeled fields are discarded. The
|
||||
// fallback representation is subject to change in v2 for lossless round trips.
|
||||
//
|
||||
// Since: Bot API 10.1
|
||||
func UnmarshalRichBlock(data []byte) (RichBlock, error) {
|
||||
if err := validateRichJSON(data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return unmarshalRichBlock(data)
|
||||
}
|
||||
|
||||
func unmarshalRichBlock(data []byte) (RichBlock, error) {
|
||||
var head struct {
|
||||
Type string `json:"type"`
|
||||
Text json.RawMessage `json:"text"`
|
||||
@@ -454,8 +477,19 @@ func UnmarshalRichBlock(data []byte) (RichBlock, error) {
|
||||
|
||||
// UnmarshalRichMessage parses a root RichMessage from JSON.
|
||||
//
|
||||
// For v1 compatibility, missing and null blocks are accepted as an empty
|
||||
// message. This permissive behavior is subject to change in v2; use
|
||||
// UnmarshalRichMessageStrict when validating untrusted input.
|
||||
//
|
||||
// Since: Bot API 10.1
|
||||
func UnmarshalRichMessage(data []byte) (RichMessage, error) {
|
||||
if err := validateRichJSON(data); err != nil {
|
||||
return RichMessage{}, err
|
||||
}
|
||||
return unmarshalRichMessage(data)
|
||||
}
|
||||
|
||||
func unmarshalRichMessage(data []byte) (RichMessage, error) {
|
||||
var raw struct {
|
||||
Blocks json.RawMessage `json:"blocks"`
|
||||
IsRTL bool `json:"is_rtl"`
|
||||
@@ -470,6 +504,28 @@ func UnmarshalRichMessage(data []byte) (RichMessage, error) {
|
||||
return RichMessage{blocks, raw.IsRTL}, nil
|
||||
}
|
||||
|
||||
// UnmarshalRichMessageStrict parses a RichMessage and requires a non-null blocks array.
|
||||
//
|
||||
// Since: Bot API 10.1
|
||||
func UnmarshalRichMessageStrict(data []byte) (RichMessage, error) {
|
||||
if err := validateRichJSON(data); err != nil {
|
||||
return RichMessage{}, err
|
||||
}
|
||||
var root map[string]json.RawMessage
|
||||
if err := json.Unmarshal(data, &root); err != nil {
|
||||
return RichMessage{}, fmt.Errorf("richmessage: %w", err)
|
||||
}
|
||||
blocks, ok := root["blocks"]
|
||||
if !ok || bytes.Equal(bytes.TrimSpace(blocks), []byte("null")) {
|
||||
return RichMessage{}, errors.New("richmessage: blocks must be a non-null array")
|
||||
}
|
||||
var rawBlocks []json.RawMessage
|
||||
if err := json.Unmarshal(blocks, &rawBlocks); err != nil {
|
||||
return RichMessage{}, errors.New("richmessage: blocks must be an array")
|
||||
}
|
||||
return unmarshalRichMessage(data)
|
||||
}
|
||||
|
||||
// UnmarshalJSON implements json.Unmarshaler.
|
||||
//
|
||||
// Since: Bot API 10.1
|
||||
@@ -491,7 +547,7 @@ func parseOptRichText(raw json.RawMessage) (RichText, error) {
|
||||
if len(raw) == 0 || string(raw) == "null" {
|
||||
return nil, nil
|
||||
}
|
||||
return UnmarshalRichText(raw)
|
||||
return unmarshalRichText(raw)
|
||||
}
|
||||
|
||||
func unmarshalRichBlocks(raw json.RawMessage) ([]RichBlock, error) {
|
||||
@@ -504,7 +560,7 @@ func unmarshalRichBlocks(raw json.RawMessage) ([]RichBlock, error) {
|
||||
}
|
||||
blocks := make([]RichBlock, len(raws))
|
||||
for i, r := range raws {
|
||||
b, err := UnmarshalRichBlock(r)
|
||||
b, err := unmarshalRichBlock(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -512,3 +568,35 @@ func unmarshalRichBlocks(raw json.RawMessage) ([]RichBlock, error) {
|
||||
}
|
||||
return blocks, nil
|
||||
}
|
||||
|
||||
func validateRichJSON(data []byte) error {
|
||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
||||
depth := 0
|
||||
nodes := 0
|
||||
for {
|
||||
token, err := decoder.Token()
|
||||
if errors.Is(err, io.EOF) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nodes++
|
||||
if nodes > maximumRichJSONNodes {
|
||||
return fmt.Errorf("%w: maximum %d", ErrRichJSONNodes, maximumRichJSONNodes)
|
||||
}
|
||||
delim, ok := token.(json.Delim)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
switch delim {
|
||||
case '{', '[':
|
||||
depth++
|
||||
if depth > maximumRichJSONDepth {
|
||||
return fmt.Errorf("%w: maximum %d", ErrRichJSONDepth, maximumRichJSONDepth)
|
||||
}
|
||||
case '}', ']':
|
||||
depth--
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user