github.com/MetalBlockchain/metalgo@v1.11.9/utils/formatting/encoding.go (about)

     1  // Copyright (C) 2019-2024, Ava Labs, Inc. All rights reserved.
     2  // See the file LICENSE for licensing terms.
     3  
     4  package formatting
     5  
     6  import (
     7  	"bytes"
     8  	"encoding/hex"
     9  	"errors"
    10  	"fmt"
    11  	"math"
    12  	"strings"
    13  
    14  	"github.com/MetalBlockchain/metalgo/utils/hashing"
    15  )
    16  
    17  const (
    18  	hexPrefix   = "0x"
    19  	checksumLen = 4
    20  )
    21  
    22  var (
    23  	errEncodingOverFlow            = errors.New("encoding overflow")
    24  	errInvalidEncoding             = errors.New("invalid encoding")
    25  	errUnsupportedEncodingInMethod = errors.New("unsupported encoding in method")
    26  	errMissingChecksum             = errors.New("input string is smaller than the checksum size")
    27  	errBadChecksum                 = errors.New("invalid input checksum")
    28  	errMissingHexPrefix            = errors.New("missing 0x prefix to hex encoding")
    29  )
    30  
    31  // Encoding defines how bytes are converted to a string and vice versa
    32  type Encoding uint8
    33  
    34  const (
    35  	// Hex specifies a hex plus 4 byte checksum encoding format
    36  	Hex Encoding = iota
    37  	// HexNC specifies a hex encoding format
    38  	HexNC
    39  	// HexC specifies a hex plus 4 byte checksum encoding format
    40  	HexC
    41  	// JSON specifies the JSON encoding format
    42  	JSON
    43  )
    44  
    45  func (enc Encoding) String() string {
    46  	switch enc {
    47  	case Hex:
    48  		return "hex"
    49  	case HexNC:
    50  		return "hexnc"
    51  	case HexC:
    52  		return "hexc"
    53  	case JSON:
    54  		return "json"
    55  	default:
    56  		return errInvalidEncoding.Error()
    57  	}
    58  }
    59  
    60  func (enc Encoding) valid() bool {
    61  	switch enc {
    62  	case Hex, HexNC, HexC, JSON:
    63  		return true
    64  	}
    65  	return false
    66  }
    67  
    68  func (enc Encoding) MarshalJSON() ([]byte, error) {
    69  	if !enc.valid() {
    70  		return nil, errInvalidEncoding
    71  	}
    72  	return []byte(`"` + enc.String() + `"`), nil
    73  }
    74  
    75  func (enc *Encoding) UnmarshalJSON(b []byte) error {
    76  	str := string(b)
    77  	if str == "null" {
    78  		return nil
    79  	}
    80  	switch strings.ToLower(str) {
    81  	case `"hex"`:
    82  		*enc = Hex
    83  	case `"hexnc"`:
    84  		*enc = HexNC
    85  	case `"hexc"`:
    86  		*enc = HexC
    87  	case `"json"`:
    88  		*enc = JSON
    89  	default:
    90  		return errInvalidEncoding
    91  	}
    92  	return nil
    93  }
    94  
    95  // Encode [bytes] to a string using the given encoding format [bytes] may be
    96  // nil, in which case it will be treated the same as an empty slice.
    97  func Encode(encoding Encoding, bytes []byte) (string, error) {
    98  	if !encoding.valid() {
    99  		return "", errInvalidEncoding
   100  	}
   101  
   102  	switch encoding {
   103  	case Hex, HexC:
   104  		bytesLen := len(bytes)
   105  		if bytesLen > math.MaxInt32-checksumLen {
   106  			return "", errEncodingOverFlow
   107  		}
   108  		checked := make([]byte, bytesLen+checksumLen)
   109  		copy(checked, bytes)
   110  		copy(checked[len(bytes):], hashing.Checksum(bytes, checksumLen))
   111  		bytes = checked
   112  	}
   113  
   114  	switch encoding {
   115  	case Hex, HexNC, HexC:
   116  		return fmt.Sprintf("0x%x", bytes), nil
   117  	case JSON:
   118  		// JSON Marshal does not support []byte input and we rely on the
   119  		// router's json marshalling to marshal our interface{} into JSON
   120  		// in response. Therefore it is not supported in this call.
   121  		return "", errUnsupportedEncodingInMethod
   122  	default:
   123  		return "", errInvalidEncoding
   124  	}
   125  }
   126  
   127  // Decode [str] to bytes using the given encoding
   128  // If [str] is the empty string, returns a nil byte slice and nil error
   129  func Decode(encoding Encoding, str string) ([]byte, error) {
   130  	switch {
   131  	case !encoding.valid():
   132  		return nil, errInvalidEncoding
   133  		// TODO: remove the empty string check and enforce the correct format.
   134  	case len(str) == 0:
   135  		return nil, nil
   136  	}
   137  
   138  	var (
   139  		decodedBytes []byte
   140  		err          error
   141  	)
   142  	switch encoding {
   143  	case Hex, HexNC, HexC:
   144  		if !strings.HasPrefix(str, hexPrefix) {
   145  			return nil, errMissingHexPrefix
   146  		}
   147  		decodedBytes, err = hex.DecodeString(str[2:])
   148  	case JSON:
   149  		// JSON unmarshalling requires interface and has no return values
   150  		// contrary to this method, therefore it is not supported in this call
   151  		return nil, errUnsupportedEncodingInMethod
   152  	default:
   153  		return nil, errInvalidEncoding
   154  	}
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  
   159  	switch encoding {
   160  	case Hex, HexC:
   161  		if len(decodedBytes) < checksumLen {
   162  			return nil, errMissingChecksum
   163  		}
   164  		// Verify the checksum
   165  		rawBytes := decodedBytes[:len(decodedBytes)-checksumLen]
   166  		checksum := decodedBytes[len(decodedBytes)-checksumLen:]
   167  		if !bytes.Equal(checksum, hashing.Checksum(rawBytes, checksumLen)) {
   168  			return nil, errBadChecksum
   169  		}
   170  		decodedBytes = rawBytes
   171  	}
   172  	return decodedBytes, nil
   173  }