Skip to content

Commit

Permalink
Merge pull request #428 from blinklabs-io/feat/cbor-constructor
Browse files Browse the repository at this point in the history
feat: direct marshal/unmarshal for cbor.Constructor
  • Loading branch information
agaffney authored Nov 16, 2023
2 parents b1a9322 + abe1cdb commit c883d3b
Show file tree
Hide file tree
Showing 3 changed files with 159 additions and 28 deletions.
8 changes: 8 additions & 0 deletions cbor/cbor.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ const (
CborTagRational = 30
CborTagSet = 258
CborTagMap = 259

// Tag ranges for "alternatives"
// https://www.ietf.org/archive/id/draft-bormann-cbor-notable-tags-07.html#name-enumerated-alternative-data
CborTagAlternative1Min = 121
CborTagAlternative1Max = 127
CborTagAlternative2Min = 1280
CborTagAlternative2Max = 1400
CborTagAlternative3 = 101
)

// Create an alias for RawMessage for convenience
Expand Down
105 changes: 77 additions & 28 deletions cbor/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,35 +73,15 @@ func (v *Value) UnmarshalCBOR(data []byte) error {
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,
if (tmpTag.Number >= CborTagAlternative1Min && tmpTag.Number <= CborTagAlternative1Max) ||
(tmpTag.Number >= CborTagAlternative2Min && tmpTag.Number <= CborTagAlternative2Max) ||
tmpTag.Number == CborTagAlternative3 {
// Constructors/alternatives
var tmpConstr Constructor
if _, err := Decode(data, &tmpConstr); err != nil {
return err
}
v.value = tmpConstr
} else {
// Fall back to standard CBOR tag parsing for our supported types
var tmpTagDecode interface{}
Expand Down Expand Up @@ -263,6 +243,18 @@ type Constructor struct {
value *Value
}

func NewConstructor(constructor uint, value any) Constructor {
c := Constructor{
constructor: constructor,
}
if value != nil {
c.value = &Value{
value: value,
}
}
return c
}

func (v Constructor) Constructor() uint {
return v.constructor
}
Expand All @@ -271,6 +263,63 @@ func (v Constructor) Fields() []any {
return v.value.Value().([]any)
}

func (c Constructor) FieldsCbor() []byte {
return c.value.Cbor()
}

func (c *Constructor) UnmarshalCBOR(data []byte) error {
// 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
}
if tmpTag.Number >= CborTagAlternative1Min && tmpTag.Number <= CborTagAlternative1Max {
// Alternatives 0-6
c.constructor = uint(tmpTag.Number - CborTagAlternative1Min)
c.value = &tmpValue
} else if tmpTag.Number >= CborTagAlternative2Min && tmpTag.Number <= CborTagAlternative2Max {
// Alternatives 7-127
c.constructor = uint(tmpTag.Number - CborTagAlternative2Min + 7)
c.value = &tmpValue
} else if tmpTag.Number == CborTagAlternative3 {
// Alternatives 128+
tmpValues := tmpValue.Value().([]any)
c.constructor = uint(tmpValues[0].(uint64))
newValue := Value{
value: tmpValues[1],
}
c.value = &newValue
} else {
return fmt.Errorf("unsupported tag: %d", tmpTag.Number)
}
return nil
}

func (c Constructor) MarshalCBOR() ([]byte, error) {
var tmpTag Tag
if c.constructor <= 6 {
// Alternatives 0-6
tmpTag.Number = uint64(c.constructor + CborTagAlternative1Min)
tmpTag.Content = c.value.Value()
} else if c.constructor >= 7 && c.constructor <= 127 {
// Alternatives 7-127
tmpTag.Number = uint64(c.constructor + CborTagAlternative2Min - 7)
tmpTag.Content = c.value.Value()
} else if c.constructor >= 128 {
tmpTag.Number = CborTagAlternative3
tmpTag.Content = []any{
c.constructor,
c.value.Value(),
}
}
return Encode(&tmpTag)
}

func (v Constructor) MarshalJSON() ([]byte, error) {
tmpJson := fmt.Sprintf(`{"constructor":%d,"fields":[`, v.constructor)
tmpList := [][]byte{}
Expand Down
74 changes: 74 additions & 0 deletions cbor/value_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,77 @@ func TestLazyValueMarshalJSON(t *testing.T) {
}
}
}

var constructorTestDefs = []struct {
cborHex string
expectedObj cbor.Constructor
}{
{
// 122([1, 2, 3])
cborHex: "D87A83010203",
expectedObj: cbor.NewConstructor(
1,
[]any{uint64(1), uint64(2), uint64(3)},
),
},
{
// 1288([3, 4, 5])
cborHex: "D9050883030405",
expectedObj: cbor.NewConstructor(
15,
[]any{uint64(3), uint64(4), uint64(5)},
),
},
{
// 101([999, [6, 7]])
cborHex: "D865821903E7820607",
expectedObj: cbor.NewConstructor(
999,
[]any{uint64(6), uint64(7)},
),
},
}

func TestConstructorDecode(t *testing.T) {
for _, testDef := range constructorTestDefs {
cborData, err := hex.DecodeString(testDef.cborHex)
if err != nil {
t.Fatalf("failed to decode CBOR hex: %s", err)
}
var tmpConstr cbor.Constructor
if _, err := cbor.Decode(cborData, &tmpConstr); err != nil {
t.Fatalf("failed to decode CBOR data: %s", err)
}
if tmpConstr.Constructor() != testDef.expectedObj.Constructor() {
t.Fatalf(
"did not get expected constructor number, got %d, wanted %d",
tmpConstr.Constructor(),
testDef.expectedObj.Constructor(),
)
}
if !reflect.DeepEqual(tmpConstr.Fields(), testDef.expectedObj.Fields()) {
t.Fatalf(
"did not decode to expected fields\n got: %#v\n wanted: %#v",
tmpConstr.Fields(),
testDef.expectedObj.Fields(),
)
}
}
}

func TestConstructorEncode(t *testing.T) {
for _, testDef := range constructorTestDefs {
cborData, err := cbor.Encode(&testDef.expectedObj)
if err != nil {
t.Fatalf("failed to encode object to CBOR: %s", err)
}
cborDataHex := hex.EncodeToString(cborData)
if cborDataHex != strings.ToLower(testDef.cborHex) {
t.Fatalf(
"did not encode to expected CBOR\n got: %s\n wanted: %s",
cborDataHex,
strings.ToLower(testDef.cborHex),
)
}
}
}

0 comments on commit c883d3b

Please sign in to comment.