167 lines
4.0 KiB
Go
167 lines
4.0 KiB
Go
package catalog
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"regexp"
|
|
"sort"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
var integerJSONNumber = regexp.MustCompile(`^-?(0|[1-9][0-9]*)$`)
|
|
|
|
func parseRestrictedJSON(data []byte) (any, error) {
|
|
if !utf8.Valid(data) {
|
|
return nil, fmt.Errorf("%w: input is not valid UTF-8", ErrInvalidDocument)
|
|
}
|
|
decoder := json.NewDecoder(bytes.NewReader(data))
|
|
decoder.UseNumber()
|
|
|
|
value, err := decodeJSONValue(decoder)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if _, err := decoder.Token(); err != io.EOF {
|
|
if err == nil {
|
|
return nil, fmt.Errorf("%w: trailing JSON value", ErrInvalidDocument)
|
|
}
|
|
return nil, fmt.Errorf("%w: trailing data: %v", ErrInvalidDocument, err)
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func decodeJSONValue(decoder *json.Decoder) (any, error) {
|
|
token, err := decoder.Token()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrInvalidDocument, err)
|
|
}
|
|
|
|
switch value := token.(type) {
|
|
case json.Delim:
|
|
switch value {
|
|
case '{':
|
|
object := make(map[string]any)
|
|
for decoder.More() {
|
|
keyToken, err := decoder.Token()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: object key: %v", ErrInvalidDocument, err)
|
|
}
|
|
key, ok := keyToken.(string)
|
|
if !ok {
|
|
return nil, fmt.Errorf("%w: object key is not a string", ErrInvalidDocument)
|
|
}
|
|
if _, exists := object[key]; exists {
|
|
return nil, fmt.Errorf("%w: %q", ErrDuplicateField, key)
|
|
}
|
|
child, err := decodeJSONValue(decoder)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
object[key] = child
|
|
}
|
|
end, err := decoder.Token()
|
|
if err != nil || end != json.Delim('}') {
|
|
return nil, fmt.Errorf("%w: unterminated object", ErrInvalidDocument)
|
|
}
|
|
return object, nil
|
|
case '[':
|
|
var array []any
|
|
for decoder.More() {
|
|
child, err := decodeJSONValue(decoder)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
array = append(array, child)
|
|
}
|
|
end, err := decoder.Token()
|
|
if err != nil || end != json.Delim(']') {
|
|
return nil, fmt.Errorf("%w: unterminated array", ErrInvalidDocument)
|
|
}
|
|
return array, nil
|
|
default:
|
|
return nil, fmt.Errorf("%w: unexpected delimiter %q", ErrInvalidDocument, value)
|
|
}
|
|
case json.Number:
|
|
if !integerJSONNumber.MatchString(string(value)) {
|
|
return nil, fmt.Errorf("%w: %q", ErrUnsupportedNumber, value)
|
|
}
|
|
return value, nil
|
|
case string, bool, nil:
|
|
return value, nil
|
|
default:
|
|
return nil, fmt.Errorf("%w: unsupported token %T", ErrInvalidDocument, token)
|
|
}
|
|
}
|
|
|
|
func canonicalJSON(value any) ([]byte, error) {
|
|
var buffer bytes.Buffer
|
|
if err := appendCanonicalJSON(&buffer, value); err != nil {
|
|
return nil, err
|
|
}
|
|
return buffer.Bytes(), nil
|
|
}
|
|
|
|
func appendCanonicalJSON(buffer *bytes.Buffer, value any) error {
|
|
switch value := value.(type) {
|
|
case nil:
|
|
buffer.WriteString("null")
|
|
case bool:
|
|
if value {
|
|
buffer.WriteString("true")
|
|
} else {
|
|
buffer.WriteString("false")
|
|
}
|
|
case string:
|
|
encoded, err := json.Marshal(value)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: encode string: %v", ErrInvalidDocument, err)
|
|
}
|
|
buffer.Write(encoded)
|
|
case json.Number:
|
|
if !integerJSONNumber.MatchString(string(value)) {
|
|
return fmt.Errorf("%w: %q", ErrUnsupportedNumber, value)
|
|
}
|
|
buffer.WriteString(string(value))
|
|
case []any:
|
|
buffer.WriteByte('[')
|
|
for index, child := range value {
|
|
if index > 0 {
|
|
buffer.WriteByte(',')
|
|
}
|
|
if err := appendCanonicalJSON(buffer, child); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
buffer.WriteByte(']')
|
|
case map[string]any:
|
|
keys := make([]string, 0, len(value))
|
|
for key := range value {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
|
|
buffer.WriteByte('{')
|
|
for index, key := range keys {
|
|
if index > 0 {
|
|
buffer.WriteByte(',')
|
|
}
|
|
encodedKey, err := json.Marshal(key)
|
|
if err != nil {
|
|
return fmt.Errorf("%w: encode key: %v", ErrInvalidDocument, err)
|
|
}
|
|
buffer.Write(encodedKey)
|
|
buffer.WriteByte(':')
|
|
if err := appendCanonicalJSON(buffer, value[key]); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
buffer.WriteByte('}')
|
|
default:
|
|
return fmt.Errorf("%w: unsupported value %T", ErrInvalidDocument, value)
|
|
}
|
|
return nil
|
|
}
|