(new): v1.2 release
Golang lint / lint (push) Successful in 11m32s

This commit is contained in:
2026-08-19 14:58:25 +03:00
parent f03a081ed6
commit 29b208eeec
79 changed files with 7301 additions and 2060 deletions
+94 -6
View File
@@ -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--
}
}
}