Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: generate JSON for arbitrary CBOR #355

Merged
merged 1 commit into from
Jul 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 {
agaffney marked this conversation as resolved.
Show resolved Hide resolved
// 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,
Dismissed Show dismissed Hide dismissed
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