mirror of
https://github.com/luxfi/formatting.git
synced 2026-10-30 16:55:53 +00:00
MarshalJSON already wrote the word in quotes, so the encoding has always had a written form; it just had nowhere to be read from except JSON. A URL carries text and no JSON, so a read served as a GET asks with ?encoding=json and a binder converting by kind read a uint8, failed, and left the request on Hex — which is what zero is. A wrong answer with a 200 on it. The mapping now lives once, in UnmarshalText, and UnmarshalJSON unquotes and calls it. The JSON wire is untouched: encoding/json prefers a json.Unmarshaler over a text one, and both spellings produce the same four words they always did.
198 lines
4.9 KiB
Go
198 lines
4.9 KiB
Go
// Copyright (C) 2019-2025, Lux Industries, Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package formatting
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/luxfi/crypto/hash"
|
|
)
|
|
|
|
const (
|
|
hexPrefix = "0x"
|
|
checksumLen = 4
|
|
)
|
|
|
|
var (
|
|
errEncodingOverFlow = errors.New("encoding overflow")
|
|
errInvalidEncoding = errors.New("invalid encoding")
|
|
errUnsupportedEncodingInMethod = errors.New("unsupported encoding in method")
|
|
errMissingChecksum = errors.New("input string is smaller than the checksum size")
|
|
errBadChecksum = errors.New("invalid input checksum")
|
|
errMissingHexPrefix = errors.New("missing 0x prefix to hex encoding")
|
|
)
|
|
|
|
// Encoding defines how bytes are converted to a string and vice versa
|
|
type Encoding uint8
|
|
|
|
const (
|
|
// Hex specifies a hex plus 4 byte checksum encoding format
|
|
Hex Encoding = iota
|
|
// HexNC specifies a hex encoding format
|
|
HexNC
|
|
// HexC specifies a hex plus 4 byte checksum encoding format
|
|
HexC
|
|
// JSON specifies the JSON encoding format
|
|
JSON
|
|
)
|
|
|
|
func (enc Encoding) String() string {
|
|
switch enc {
|
|
case Hex:
|
|
return "hex"
|
|
case HexNC:
|
|
return "hexnc"
|
|
case HexC:
|
|
return "hexc"
|
|
case JSON:
|
|
return "json"
|
|
default:
|
|
return errInvalidEncoding.Error()
|
|
}
|
|
}
|
|
|
|
func (enc Encoding) valid() bool {
|
|
switch enc {
|
|
case Hex, HexNC, HexC, JSON:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// MarshalText and UnmarshalText are the encoding's written form, which is the
|
|
// one it has always had: MarshalJSON writes this same word in quotes. It is
|
|
// said here as well because a URL carries text and no JSON. A read served as a
|
|
// GET asks for its encoding with ?encoding=json, and an encoding read as the
|
|
// number its kind is left every such request on Hex, which is what zero is.
|
|
func (enc Encoding) MarshalText() ([]byte, error) {
|
|
if !enc.valid() {
|
|
return nil, errInvalidEncoding
|
|
}
|
|
return []byte(enc.String()), nil
|
|
}
|
|
|
|
func (enc *Encoding) UnmarshalText(text []byte) error {
|
|
switch strings.ToLower(string(text)) {
|
|
case "hex":
|
|
*enc = Hex
|
|
case "hexnc":
|
|
*enc = HexNC
|
|
case "hexc":
|
|
*enc = HexC
|
|
case "json":
|
|
*enc = JSON
|
|
default:
|
|
return errInvalidEncoding
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (enc Encoding) MarshalJSON() ([]byte, error) {
|
|
text, err := enc.MarshalText()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return []byte(`"` + string(text) + `"`), nil
|
|
}
|
|
|
|
// UnmarshalJSON reads the same word, quoted. The mapping is in UnmarshalText
|
|
// and is not written a second time; null is JSON's spelling of an absent field,
|
|
// which leaves the encoding as it was.
|
|
func (enc *Encoding) UnmarshalJSON(b []byte) error {
|
|
str := string(b)
|
|
if str == "null" {
|
|
return nil
|
|
}
|
|
word, err := strconv.Unquote(str)
|
|
if err != nil {
|
|
return errInvalidEncoding
|
|
}
|
|
return enc.UnmarshalText([]byte(word))
|
|
}
|
|
|
|
// Encode [bytes] to a string using the given encoding format [bytes] may be
|
|
// nil, in which case it will be treated the same as an empty slice.
|
|
func Encode(encoding Encoding, bytes []byte) (string, error) {
|
|
if !encoding.valid() {
|
|
return "", errInvalidEncoding
|
|
}
|
|
|
|
switch encoding {
|
|
case Hex, HexC:
|
|
bytesLen := len(bytes)
|
|
if bytesLen > math.MaxInt32-checksumLen {
|
|
return "", errEncodingOverFlow
|
|
}
|
|
checked := make([]byte, bytesLen+checksumLen)
|
|
copy(checked, bytes)
|
|
copy(checked[len(bytes):], hash.Checksum(bytes, checksumLen))
|
|
bytes = checked
|
|
}
|
|
|
|
switch encoding {
|
|
case Hex, HexNC, HexC:
|
|
return fmt.Sprintf("0x%x", bytes), nil
|
|
case JSON:
|
|
// JSON Marshal does not support []byte input and we rely on the
|
|
// router's json marshalling to marshal our interface{} into JSON
|
|
// in response. Therefore it is not supported in this call.
|
|
return "", errUnsupportedEncodingInMethod
|
|
default:
|
|
return "", errInvalidEncoding
|
|
}
|
|
}
|
|
|
|
// Decode [str] to bytes using the given encoding
|
|
// If [str] is the empty string, returns a nil byte slice and nil error
|
|
func Decode(encoding Encoding, str string) ([]byte, error) {
|
|
switch {
|
|
case !encoding.valid():
|
|
return nil, errInvalidEncoding
|
|
case len(str) == 0:
|
|
return nil, nil
|
|
}
|
|
|
|
var (
|
|
decodedBytes []byte
|
|
err error
|
|
)
|
|
switch encoding {
|
|
case Hex, HexNC, HexC:
|
|
if !strings.HasPrefix(str, hexPrefix) {
|
|
return nil, errMissingHexPrefix
|
|
}
|
|
decodedBytes, err = hex.DecodeString(str[2:])
|
|
case JSON:
|
|
// JSON unmarshalling requires interface and has no return values
|
|
// contrary to this method, therefore it is not supported in this call
|
|
return nil, errUnsupportedEncodingInMethod
|
|
default:
|
|
return nil, errInvalidEncoding
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch encoding {
|
|
case Hex, HexC:
|
|
if len(decodedBytes) < checksumLen {
|
|
return nil, errMissingChecksum
|
|
}
|
|
// Verify the checksum
|
|
rawBytes := decodedBytes[:len(decodedBytes)-checksumLen]
|
|
checksum := decodedBytes[len(decodedBytes)-checksumLen:]
|
|
if !bytes.Equal(checksum, hash.Checksum(rawBytes, checksumLen)) {
|
|
return nil, errBadChecksum
|
|
}
|
|
decodedBytes = rawBytes
|
|
}
|
|
return decodedBytes, nil
|
|
}
|