Skip to content

Commit

Permalink
Merge pull request #355 from blinklabs-io/feat/cbor-value-structure
Browse files Browse the repository at this point in the history
feat: generate JSON for arbitrary CBOR
  • Loading branch information
agaffney authored Jul 1, 2023
2 parents 05743ac + 2244f08 commit cf43d34
Show file tree
Hide file tree
Showing 7 changed files with 464 additions and 63 deletions.
4 changes: 4 additions & 0 deletions cbor/cbor.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ const (

// Max value able to be stored in a single byte without type prefix
CBOR_MAX_UINT_SIMPLE uint8 = 0x17

// Useful tag numbers
CborTagSet = 258
CborTagMap = 259
)

// Create an alias for RawMessage for convenience
Expand Down
2 changes: 1 addition & 1 deletion cbor/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ func DecodeIdFromList(cborData []byte) (int, error) {
return 0, err
}
// Make sure that the value is actually numeric
switch v := tmp.Value.([]interface{})[0].(type) {
switch v := tmp.Value().([]interface{})[0].(type) {
// The upstream CBOR library uses uint64 by default for numeric values
case uint64:
return int(v), nil
Expand Down
266 changes: 216 additions & 50 deletions cbor/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,88 +15,94 @@
package cbor

import (
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"strings"
)

// Helpful wrapper for parsing arbitrary CBOR data which may contain types that
// cannot be easily represented in Go (such as maps with bytestring keys)
type Value struct {
Value interface{}
value interface{}
// We store this as a string so that the type is still hashable for use as map keys
cborData string
}

func (v *Value) UnmarshalCBOR(data []byte) (err error) {
func (v *Value) UnmarshalCBOR(data []byte) error {
// Save the original CBOR
v.cborData = string(data[:])
cborType := data[0] & CBOR_TYPE_MASK
switch cborType {
case CBOR_TYPE_MAP:
// There are certain types that cannot be used as map keys in Go but are valid in CBOR. Trying to
// parse CBOR containing a map with keys of one of those types will cause a panic. We setup this
// deferred function to recover from a possible panic and return an error
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("decode failure, probably due to type unsupported by Go: %v", r)
}
}()
tmpValue := map[Value]Value{}
if _, err := Decode(data, &tmpValue); err != nil {
return err
}
// Extract actual value from each child value
newValue := map[interface{}]interface{}{}
for key, value := range tmpValue {
newValue[key.Value] = value.Value
}
v.Value = newValue
return v.processMap(data)
case CBOR_TYPE_ARRAY:
tmpValue := []Value{}
if _, err := Decode(data, &tmpValue); err != nil {
return err
}
// Extract actual value from each child value
newValue := []interface{}{}
for _, value := range tmpValue {
newValue = append(newValue, value.Value)
}
v.Value = newValue
return v.processArray(data)
case CBOR_TYPE_TEXT_STRING:
var tmpValue string
if _, err := Decode(data, &tmpValue); err != nil {
return err
}
v.Value = tmpValue
v.value = tmpValue
case CBOR_TYPE_BYTE_STRING:
// Use our custom type which stores the bytestring in a way that allows it to be used as a map key
var tmpValue ByteString
if _, err := Decode(data, &tmpValue); err != nil {
return err
}
v.Value = tmpValue
v.value = tmpValue
case CBOR_TYPE_TAG:
// Parse as a raw tag to get number and nested CBOR data
tmpTag := RawTag{}
if _, err := Decode(data, &tmpTag); err != nil {
return err
}
// Parse the tag value via our custom Value object to handle problem types
tmpValue := Value{}
if _, err := Decode(tmpTag.Content, &tmpValue); err != nil {
return err
}
// Create new tag object with decoded value
newValue := Tag{
Number: tmpTag.Number,
Content: tmpValue.Value,
switch tmpTag.Number {
case CborTagSet:
return v.processArray(tmpTag.Content)
case CborTagMap:
return v.processMap(tmpTag.Content)
default:
// Parse the tag value via our custom Value object to handle problem types
tmpValue := Value{}
if _, err := Decode(tmpTag.Content, &tmpValue); err != nil {
return err
}
// These tag numbers correspond to the Enumerated Alternative Data Items notable CBOR tags. These
// are often used in Plutus script datums
// https://www.ietf.org/archive/id/draft-bormann-cbor-notable-tags-07.html#name-enumerated-alternative-data
if tmpTag.Number >= 121 && tmpTag.Number <= 127 {
// Alternatives 0-6
v.value = Constructor{
constructor: uint(tmpTag.Number - 121),
value: &tmpValue,
}
} else if tmpTag.Number >= 1280 && tmpTag.Number <= 1400 {
// Alternatives 7-127
v.value = Constructor{
constructor: uint(tmpTag.Number - 1280 + 7),
value: &tmpValue,
}
} else if tmpTag.Number == 101 {
// Alternatives 128+
newValue := Value{
value: tmpValue.Value().([]interface{})[1],
}
v.value = Constructor{
constructor: tmpValue.Value().([]interface{})[0].(uint),
value: &newValue,
}
} else {
return fmt.Errorf("unsupported CBOR tag: %d", tmpTag.Number)
}
}
v.Value = newValue
default:
var tmpValue interface{}
if _, err := Decode(data, &tmpValue); err != nil {
return err
}
v.Value = tmpValue
v.value = tmpValue
}
return nil
}
Expand All @@ -105,19 +111,179 @@ func (v Value) Cbor() []byte {
return []byte(v.cborData)
}

func (v Value) Value() interface{} {
return v.value
}

func (v Value) MarshalJSON() ([]byte, error) {
var tmpJson string
if v.value != nil {
astJson, err := generateAstJson(v.value)
if err != nil {
return nil, err
}
tmpJson = fmt.Sprintf(
`{"cbor":"%s","json":%s}`,
hex.EncodeToString([]byte(v.cborData)),
astJson,
)
} else {
tmpJson = fmt.Sprintf(
`{"cbor":"%s"}`,
hex.EncodeToString([]byte(v.cborData)),
)
}
return []byte(tmpJson), nil
}

func (v *Value) processMap(data []byte) (err error) {
// There are certain types that cannot be used as map keys in Go but are valid in CBOR. Trying to
// parse CBOR containing a map with keys of one of those types will cause a panic. We setup this
// deferred function to recover from a possible panic and return an error
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("decode failure, probably due to type unsupported by Go: %v", r)
}
}()
tmpValue := map[Value]Value{}
if _, err = Decode(data, &tmpValue); err != nil {
return err
}
// Extract actual value from each child value
newValue := map[interface{}]interface{}{}
for key, value := range tmpValue {
newValue[key.Value()] = value.Value()
}
v.value = newValue
return nil
}

func (v *Value) processArray(data []byte) error {
tmpValue := []Value{}
if _, err := Decode(data, &tmpValue); err != nil {
return err
}
// Extract actual value from each child value
newValue := []interface{}{}
for _, value := range tmpValue {
newValue = append(newValue, value.Value())
}
v.value = newValue
return nil
}

func generateAstJson(obj interface{}) ([]byte, error) {
tmpJsonObj := map[string]interface{}{}
switch v := obj.(type) {
case ByteString:
tmpJsonObj["bytes"] = hex.EncodeToString(v.Bytes())
case []interface{}:
tmpJson := `{"list":[`
for idx, val := range v {
tmpVal, err := generateAstJson(val)
if err != nil {
return nil, err
}
tmpJson += string(tmpVal)
if idx != (len(v) - 1) {
tmpJson += `,`
}
}
tmpJson += `]}`
return []byte(tmpJson), nil
case map[interface{}]interface{}:
tmpItems := []string{}
for key, val := range v {
keyAstJson, err := generateAstJson(key)
if err != nil {
return nil, err
}
valAstJson, err := generateAstJson(val)
if err != nil {
return nil, err
}
tmpJson := fmt.Sprintf(
`{"k":%s,"v":%s}`,
keyAstJson,
valAstJson,
)
tmpItems = append(tmpItems, string(tmpJson))
}
// We naively sort the rendered map items to give consistent ordering
sort.Strings(tmpItems)
tmpJson := fmt.Sprintf(
`{"map":[%s]}`,
strings.Join(tmpItems, ","),
)
return []byte(tmpJson), nil
case Constructor:
return json.Marshal(obj)
case int, uint, uint64, int64:
tmpJsonObj["int"] = v
case bool:
tmpJsonObj["bool"] = v
case string:
tmpJsonObj["string"] = v
default:
return nil, fmt.Errorf("unknown data type (%T) for value: %#v", obj, obj)
}
return json.Marshal(&tmpJsonObj)
}

type Constructor struct {
constructor uint
value *Value
}

func (v Constructor) MarshalJSON() ([]byte, error) {
tmpJson := fmt.Sprintf(`{"constructor":%d,"fields":[`, v.constructor)
tmpList := [][]byte{}
for _, val := range v.value.Value().([]any) {
tmpVal, err := generateAstJson(val)
if err != nil {
return nil, err
}
tmpList = append(tmpList, tmpVal)
}
for idx, val := range tmpList {
tmpJson += string(val)
if idx != (len(tmpList) - 1) {
tmpJson += `,`
}
}
tmpJson += `]}`
return []byte(tmpJson), nil
}

type LazyValue struct {
*Value
value *Value
}

func (l *LazyValue) UnmarshalCBOR(data []byte) error {
if l.Value == nil {
l.Value = &Value{}
if l.value == nil {
l.value = &Value{}
}
l.cborData = string(data[:])
l.value.cborData = string(data[:])
return nil
}

func (l *LazyValue) Decode() (*Value, error) {
err := l.Value.UnmarshalCBOR([]byte(l.cborData))
return l.Value, err
func (l *LazyValue) MarshalJSON() ([]byte, error) {
if l.Value() == nil {
// Try to decode if we can, but don't blow up if we can't
_, _ = l.Decode()
}
return l.value.MarshalJSON()
}

func (l *LazyValue) Decode() (interface{}, error) {
err := l.value.UnmarshalCBOR([]byte(l.value.cborData))
return l.Value(), err
}

func (l *LazyValue) Value() interface{} {
return l.value.Value()
}

func (l *LazyValue) Cbor() []byte {
return l.value.Cbor()
}
Loading

0 comments on commit cf43d34

Please sign in to comment.