Skip to content

Commit cf43d34

Browse files
authored
Merge pull request #355 from blinklabs-io/feat/cbor-value-structure
feat: generate JSON for arbitrary CBOR
2 parents 05743ac + 2244f08 commit cf43d34

File tree

7 files changed

+464
-63
lines changed

7 files changed

+464
-63
lines changed

cbor/cbor.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ const (
3030

3131
// Max value able to be stored in a single byte without type prefix
3232
CBOR_MAX_UINT_SIMPLE uint8 = 0x17
33+
34+
// Useful tag numbers
35+
CborTagSet = 258
36+
CborTagMap = 259
3337
)
3438

3539
// Create an alias for RawMessage for convenience

cbor/decode.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func DecodeIdFromList(cborData []byte) (int, error) {
6464
return 0, err
6565
}
6666
// Make sure that the value is actually numeric
67-
switch v := tmp.Value.([]interface{})[0].(type) {
67+
switch v := tmp.Value().([]interface{})[0].(type) {
6868
// The upstream CBOR library uses uint64 by default for numeric values
6969
case uint64:
7070
return int(v), nil

cbor/value.go

Lines changed: 216 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -15,88 +15,94 @@
1515
package cbor
1616

1717
import (
18+
"encoding/hex"
19+
"encoding/json"
1820
"fmt"
21+
"sort"
22+
"strings"
1923
)
2024

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

29-
func (v *Value) UnmarshalCBOR(data []byte) (err error) {
33+
func (v *Value) UnmarshalCBOR(data []byte) error {
3034
// Save the original CBOR
3135
v.cborData = string(data[:])
3236
cborType := data[0] & CBOR_TYPE_MASK
3337
switch cborType {
3438
case CBOR_TYPE_MAP:
35-
// There are certain types that cannot be used as map keys in Go but are valid in CBOR. Trying to
36-
// parse CBOR containing a map with keys of one of those types will cause a panic. We setup this
37-
// deferred function to recover from a possible panic and return an error
38-
defer func() {
39-
if r := recover(); r != nil {
40-
err = fmt.Errorf("decode failure, probably due to type unsupported by Go: %v", r)
41-
}
42-
}()
43-
tmpValue := map[Value]Value{}
44-
if _, err := Decode(data, &tmpValue); err != nil {
45-
return err
46-
}
47-
// Extract actual value from each child value
48-
newValue := map[interface{}]interface{}{}
49-
for key, value := range tmpValue {
50-
newValue[key.Value] = value.Value
51-
}
52-
v.Value = newValue
39+
return v.processMap(data)
5340
case CBOR_TYPE_ARRAY:
54-
tmpValue := []Value{}
55-
if _, err := Decode(data, &tmpValue); err != nil {
56-
return err
57-
}
58-
// Extract actual value from each child value
59-
newValue := []interface{}{}
60-
for _, value := range tmpValue {
61-
newValue = append(newValue, value.Value)
62-
}
63-
v.Value = newValue
41+
return v.processArray(data)
6442
case CBOR_TYPE_TEXT_STRING:
6543
var tmpValue string
6644
if _, err := Decode(data, &tmpValue); err != nil {
6745
return err
6846
}
69-
v.Value = tmpValue
47+
v.value = tmpValue
7048
case CBOR_TYPE_BYTE_STRING:
7149
// Use our custom type which stores the bytestring in a way that allows it to be used as a map key
7250
var tmpValue ByteString
7351
if _, err := Decode(data, &tmpValue); err != nil {
7452
return err
7553
}
76-
v.Value = tmpValue
54+
v.value = tmpValue
7755
case CBOR_TYPE_TAG:
7856
// Parse as a raw tag to get number and nested CBOR data
7957
tmpTag := RawTag{}
8058
if _, err := Decode(data, &tmpTag); err != nil {
8159
return err
8260
}
83-
// Parse the tag value via our custom Value object to handle problem types
84-
tmpValue := Value{}
85-
if _, err := Decode(tmpTag.Content, &tmpValue); err != nil {
86-
return err
87-
}
88-
// Create new tag object with decoded value
89-
newValue := Tag{
90-
Number: tmpTag.Number,
91-
Content: tmpValue.Value,
61+
switch tmpTag.Number {
62+
case CborTagSet:
63+
return v.processArray(tmpTag.Content)
64+
case CborTagMap:
65+
return v.processMap(tmpTag.Content)
66+
default:
67+
// Parse the tag value via our custom Value object to handle problem types
68+
tmpValue := Value{}
69+
if _, err := Decode(tmpTag.Content, &tmpValue); err != nil {
70+
return err
71+
}
72+
// These tag numbers correspond to the Enumerated Alternative Data Items notable CBOR tags. These
73+
// are often used in Plutus script datums
74+
// https://www.ietf.org/archive/id/draft-bormann-cbor-notable-tags-07.html#name-enumerated-alternative-data
75+
if tmpTag.Number >= 121 && tmpTag.Number <= 127 {
76+
// Alternatives 0-6
77+
v.value = Constructor{
78+
constructor: uint(tmpTag.Number - 121),
79+
value: &tmpValue,
80+
}
81+
} else if tmpTag.Number >= 1280 && tmpTag.Number <= 1400 {
82+
// Alternatives 7-127
83+
v.value = Constructor{
84+
constructor: uint(tmpTag.Number - 1280 + 7),
85+
value: &tmpValue,
86+
}
87+
} else if tmpTag.Number == 101 {
88+
// Alternatives 128+
89+
newValue := Value{
90+
value: tmpValue.Value().([]interface{})[1],
91+
}
92+
v.value = Constructor{
93+
constructor: tmpValue.Value().([]interface{})[0].(uint),
94+
value: &newValue,
95+
}
96+
} else {
97+
return fmt.Errorf("unsupported CBOR tag: %d", tmpTag.Number)
98+
}
9299
}
93-
v.Value = newValue
94100
default:
95101
var tmpValue interface{}
96102
if _, err := Decode(data, &tmpValue); err != nil {
97103
return err
98104
}
99-
v.Value = tmpValue
105+
v.value = tmpValue
100106
}
101107
return nil
102108
}
@@ -105,19 +111,179 @@ func (v Value) Cbor() []byte {
105111
return []byte(v.cborData)
106112
}
107113

114+
func (v Value) Value() interface{} {
115+
return v.value
116+
}
117+
118+
func (v Value) MarshalJSON() ([]byte, error) {
119+
var tmpJson string
120+
if v.value != nil {
121+
astJson, err := generateAstJson(v.value)
122+
if err != nil {
123+
return nil, err
124+
}
125+
tmpJson = fmt.Sprintf(
126+
`{"cbor":"%s","json":%s}`,
127+
hex.EncodeToString([]byte(v.cborData)),
128+
astJson,
129+
)
130+
} else {
131+
tmpJson = fmt.Sprintf(
132+
`{"cbor":"%s"}`,
133+
hex.EncodeToString([]byte(v.cborData)),
134+
)
135+
}
136+
return []byte(tmpJson), nil
137+
}
138+
139+
func (v *Value) processMap(data []byte) (err error) {
140+
// There are certain types that cannot be used as map keys in Go but are valid in CBOR. Trying to
141+
// parse CBOR containing a map with keys of one of those types will cause a panic. We setup this
142+
// deferred function to recover from a possible panic and return an error
143+
defer func() {
144+
if r := recover(); r != nil {
145+
err = fmt.Errorf("decode failure, probably due to type unsupported by Go: %v", r)
146+
}
147+
}()
148+
tmpValue := map[Value]Value{}
149+
if _, err = Decode(data, &tmpValue); err != nil {
150+
return err
151+
}
152+
// Extract actual value from each child value
153+
newValue := map[interface{}]interface{}{}
154+
for key, value := range tmpValue {
155+
newValue[key.Value()] = value.Value()
156+
}
157+
v.value = newValue
158+
return nil
159+
}
160+
161+
func (v *Value) processArray(data []byte) error {
162+
tmpValue := []Value{}
163+
if _, err := Decode(data, &tmpValue); err != nil {
164+
return err
165+
}
166+
// Extract actual value from each child value
167+
newValue := []interface{}{}
168+
for _, value := range tmpValue {
169+
newValue = append(newValue, value.Value())
170+
}
171+
v.value = newValue
172+
return nil
173+
}
174+
175+
func generateAstJson(obj interface{}) ([]byte, error) {
176+
tmpJsonObj := map[string]interface{}{}
177+
switch v := obj.(type) {
178+
case ByteString:
179+
tmpJsonObj["bytes"] = hex.EncodeToString(v.Bytes())
180+
case []interface{}:
181+
tmpJson := `{"list":[`
182+
for idx, val := range v {
183+
tmpVal, err := generateAstJson(val)
184+
if err != nil {
185+
return nil, err
186+
}
187+
tmpJson += string(tmpVal)
188+
if idx != (len(v) - 1) {
189+
tmpJson += `,`
190+
}
191+
}
192+
tmpJson += `]}`
193+
return []byte(tmpJson), nil
194+
case map[interface{}]interface{}:
195+
tmpItems := []string{}
196+
for key, val := range v {
197+
keyAstJson, err := generateAstJson(key)
198+
if err != nil {
199+
return nil, err
200+
}
201+
valAstJson, err := generateAstJson(val)
202+
if err != nil {
203+
return nil, err
204+
}
205+
tmpJson := fmt.Sprintf(
206+
`{"k":%s,"v":%s}`,
207+
keyAstJson,
208+
valAstJson,
209+
)
210+
tmpItems = append(tmpItems, string(tmpJson))
211+
}
212+
// We naively sort the rendered map items to give consistent ordering
213+
sort.Strings(tmpItems)
214+
tmpJson := fmt.Sprintf(
215+
`{"map":[%s]}`,
216+
strings.Join(tmpItems, ","),
217+
)
218+
return []byte(tmpJson), nil
219+
case Constructor:
220+
return json.Marshal(obj)
221+
case int, uint, uint64, int64:
222+
tmpJsonObj["int"] = v
223+
case bool:
224+
tmpJsonObj["bool"] = v
225+
case string:
226+
tmpJsonObj["string"] = v
227+
default:
228+
return nil, fmt.Errorf("unknown data type (%T) for value: %#v", obj, obj)
229+
}
230+
return json.Marshal(&tmpJsonObj)
231+
}
232+
233+
type Constructor struct {
234+
constructor uint
235+
value *Value
236+
}
237+
238+
func (v Constructor) MarshalJSON() ([]byte, error) {
239+
tmpJson := fmt.Sprintf(`{"constructor":%d,"fields":[`, v.constructor)
240+
tmpList := [][]byte{}
241+
for _, val := range v.value.Value().([]any) {
242+
tmpVal, err := generateAstJson(val)
243+
if err != nil {
244+
return nil, err
245+
}
246+
tmpList = append(tmpList, tmpVal)
247+
}
248+
for idx, val := range tmpList {
249+
tmpJson += string(val)
250+
if idx != (len(tmpList) - 1) {
251+
tmpJson += `,`
252+
}
253+
}
254+
tmpJson += `]}`
255+
return []byte(tmpJson), nil
256+
}
257+
108258
type LazyValue struct {
109-
*Value
259+
value *Value
110260
}
111261

112262
func (l *LazyValue) UnmarshalCBOR(data []byte) error {
113-
if l.Value == nil {
114-
l.Value = &Value{}
263+
if l.value == nil {
264+
l.value = &Value{}
115265
}
116-
l.cborData = string(data[:])
266+
l.value.cborData = string(data[:])
117267
return nil
118268
}
119269

120-
func (l *LazyValue) Decode() (*Value, error) {
121-
err := l.Value.UnmarshalCBOR([]byte(l.cborData))
122-
return l.Value, err
270+
func (l *LazyValue) MarshalJSON() ([]byte, error) {
271+
if l.Value() == nil {
272+
// Try to decode if we can, but don't blow up if we can't
273+
_, _ = l.Decode()
274+
}
275+
return l.value.MarshalJSON()
276+
}
277+
278+
func (l *LazyValue) Decode() (interface{}, error) {
279+
err := l.value.UnmarshalCBOR([]byte(l.value.cborData))
280+
return l.Value(), err
281+
}
282+
283+
func (l *LazyValue) Value() interface{} {
284+
return l.value.Value()
285+
}
286+
287+
func (l *LazyValue) Cbor() []byte {
288+
return l.value.Cbor()
123289
}

0 commit comments

Comments
 (0)