Files
soft_quay/core/catalog/canonical.go
T

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
}