Skip to content

Commit c883d3b

Browse files
authored
Merge pull request #428 from blinklabs-io/feat/cbor-constructor
feat: direct marshal/unmarshal for cbor.Constructor
2 parents b1a9322 + abe1cdb commit c883d3b

File tree

3 files changed

+159
-28
lines changed

3 files changed

+159
-28
lines changed

cbor/cbor.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ const (
3636
CborTagRational = 30
3737
CborTagSet = 258
3838
CborTagMap = 259
39+
40+
// Tag ranges for "alternatives"
41+
// https://www.ietf.org/archive/id/draft-bormann-cbor-notable-tags-07.html#name-enumerated-alternative-data
42+
CborTagAlternative1Min = 121
43+
CborTagAlternative1Max = 127
44+
CborTagAlternative2Min = 1280
45+
CborTagAlternative2Max = 1400
46+
CborTagAlternative3 = 101
3947
)
4048

4149
// Create an alias for RawMessage for convenience

cbor/value.go

Lines changed: 77 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -73,35 +73,15 @@ func (v *Value) UnmarshalCBOR(data []byte) error {
7373
case CborTagMap:
7474
return v.processMap(tmpTag.Content)
7575
default:
76-
// Parse the tag value via our custom Value object to handle problem types
77-
tmpValue := Value{}
78-
if _, err := Decode(tmpTag.Content, &tmpValue); err != nil {
79-
return err
80-
}
81-
// These tag numbers correspond to the Enumerated Alternative Data Items notable CBOR tags. These
82-
// are often used in Plutus script datums
83-
// https://www.ietf.org/archive/id/draft-bormann-cbor-notable-tags-07.html#name-enumerated-alternative-data
84-
if tmpTag.Number >= 121 && tmpTag.Number <= 127 {
85-
// Alternatives 0-6
86-
v.value = Constructor{
87-
constructor: uint(tmpTag.Number - 121),
88-
value: &tmpValue,
89-
}
90-
} else if tmpTag.Number >= 1280 && tmpTag.Number <= 1400 {
91-
// Alternatives 7-127
92-
v.value = Constructor{
93-
constructor: uint(tmpTag.Number - 1280 + 7),
94-
value: &tmpValue,
95-
}
96-
} else if tmpTag.Number == 101 {
97-
// Alternatives 128+
98-
newValue := Value{
99-
value: tmpValue.Value().([]interface{})[1],
100-
}
101-
v.value = Constructor{
102-
constructor: tmpValue.Value().([]interface{})[0].(uint),
103-
value: &newValue,
76+
if (tmpTag.Number >= CborTagAlternative1Min && tmpTag.Number <= CborTagAlternative1Max) ||
77+
(tmpTag.Number >= CborTagAlternative2Min && tmpTag.Number <= CborTagAlternative2Max) ||
78+
tmpTag.Number == CborTagAlternative3 {
79+
// Constructors/alternatives
80+
var tmpConstr Constructor
81+
if _, err := Decode(data, &tmpConstr); err != nil {
82+
return err
10483
}
84+
v.value = tmpConstr
10585
} else {
10686
// Fall back to standard CBOR tag parsing for our supported types
10787
var tmpTagDecode interface{}
@@ -263,6 +243,18 @@ type Constructor struct {
263243
value *Value
264244
}
265245

246+
func NewConstructor(constructor uint, value any) Constructor {
247+
c := Constructor{
248+
constructor: constructor,
249+
}
250+
if value != nil {
251+
c.value = &Value{
252+
value: value,
253+
}
254+
}
255+
return c
256+
}
257+
266258
func (v Constructor) Constructor() uint {
267259
return v.constructor
268260
}
@@ -271,6 +263,63 @@ func (v Constructor) Fields() []any {
271263
return v.value.Value().([]any)
272264
}
273265

266+
func (c Constructor) FieldsCbor() []byte {
267+
return c.value.Cbor()
268+
}
269+
270+
func (c *Constructor) UnmarshalCBOR(data []byte) error {
271+
// Parse as a raw tag to get number and nested CBOR data
272+
tmpTag := RawTag{}
273+
if _, err := Decode(data, &tmpTag); err != nil {
274+
return err
275+
}
276+
// Parse the tag value via our custom Value object to handle problem types
277+
tmpValue := Value{}
278+
if _, err := Decode(tmpTag.Content, &tmpValue); err != nil {
279+
return err
280+
}
281+
if tmpTag.Number >= CborTagAlternative1Min && tmpTag.Number <= CborTagAlternative1Max {
282+
// Alternatives 0-6
283+
c.constructor = uint(tmpTag.Number - CborTagAlternative1Min)
284+
c.value = &tmpValue
285+
} else if tmpTag.Number >= CborTagAlternative2Min && tmpTag.Number <= CborTagAlternative2Max {
286+
// Alternatives 7-127
287+
c.constructor = uint(tmpTag.Number - CborTagAlternative2Min + 7)
288+
c.value = &tmpValue
289+
} else if tmpTag.Number == CborTagAlternative3 {
290+
// Alternatives 128+
291+
tmpValues := tmpValue.Value().([]any)
292+
c.constructor = uint(tmpValues[0].(uint64))
293+
newValue := Value{
294+
value: tmpValues[1],
295+
}
296+
c.value = &newValue
297+
} else {
298+
return fmt.Errorf("unsupported tag: %d", tmpTag.Number)
299+
}
300+
return nil
301+
}
302+
303+
func (c Constructor) MarshalCBOR() ([]byte, error) {
304+
var tmpTag Tag
305+
if c.constructor <= 6 {
306+
// Alternatives 0-6
307+
tmpTag.Number = uint64(c.constructor + CborTagAlternative1Min)
308+
tmpTag.Content = c.value.Value()
309+
} else if c.constructor >= 7 && c.constructor <= 127 {
310+
// Alternatives 7-127
311+
tmpTag.Number = uint64(c.constructor + CborTagAlternative2Min - 7)
312+
tmpTag.Content = c.value.Value()
313+
} else if c.constructor >= 128 {
314+
tmpTag.Number = CborTagAlternative3
315+
tmpTag.Content = []any{
316+
c.constructor,
317+
c.value.Value(),
318+
}
319+
}
320+
return Encode(&tmpTag)
321+
}
322+
274323
func (v Constructor) MarshalJSON() ([]byte, error) {
275324
tmpJson := fmt.Sprintf(`{"constructor":%d,"fields":[`, v.constructor)
276325
tmpList := [][]byte{}

cbor/value_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,77 @@ func TestLazyValueMarshalJSON(t *testing.T) {
253253
}
254254
}
255255
}
256+
257+
var constructorTestDefs = []struct {
258+
cborHex string
259+
expectedObj cbor.Constructor
260+
}{
261+
{
262+
// 122([1, 2, 3])
263+
cborHex: "D87A83010203",
264+
expectedObj: cbor.NewConstructor(
265+
1,
266+
[]any{uint64(1), uint64(2), uint64(3)},
267+
),
268+
},
269+
{
270+
// 1288([3, 4, 5])
271+
cborHex: "D9050883030405",
272+
expectedObj: cbor.NewConstructor(
273+
15,
274+
[]any{uint64(3), uint64(4), uint64(5)},
275+
),
276+
},
277+
{
278+
// 101([999, [6, 7]])
279+
cborHex: "D865821903E7820607",
280+
expectedObj: cbor.NewConstructor(
281+
999,
282+
[]any{uint64(6), uint64(7)},
283+
),
284+
},
285+
}
286+
287+
func TestConstructorDecode(t *testing.T) {
288+
for _, testDef := range constructorTestDefs {
289+
cborData, err := hex.DecodeString(testDef.cborHex)
290+
if err != nil {
291+
t.Fatalf("failed to decode CBOR hex: %s", err)
292+
}
293+
var tmpConstr cbor.Constructor
294+
if _, err := cbor.Decode(cborData, &tmpConstr); err != nil {
295+
t.Fatalf("failed to decode CBOR data: %s", err)
296+
}
297+
if tmpConstr.Constructor() != testDef.expectedObj.Constructor() {
298+
t.Fatalf(
299+
"did not get expected constructor number, got %d, wanted %d",
300+
tmpConstr.Constructor(),
301+
testDef.expectedObj.Constructor(),
302+
)
303+
}
304+
if !reflect.DeepEqual(tmpConstr.Fields(), testDef.expectedObj.Fields()) {
305+
t.Fatalf(
306+
"did not decode to expected fields\n got: %#v\n wanted: %#v",
307+
tmpConstr.Fields(),
308+
testDef.expectedObj.Fields(),
309+
)
310+
}
311+
}
312+
}
313+
314+
func TestConstructorEncode(t *testing.T) {
315+
for _, testDef := range constructorTestDefs {
316+
cborData, err := cbor.Encode(&testDef.expectedObj)
317+
if err != nil {
318+
t.Fatalf("failed to encode object to CBOR: %s", err)
319+
}
320+
cborDataHex := hex.EncodeToString(cborData)
321+
if cborDataHex != strings.ToLower(testDef.cborHex) {
322+
t.Fatalf(
323+
"did not encode to expected CBOR\n got: %s\n wanted: %s",
324+
cborDataHex,
325+
strings.ToLower(testDef.cborHex),
326+
)
327+
}
328+
}
329+
}

0 commit comments

Comments
 (0)