Skip to content

Commit

Permalink
feat: custom CBOR tag handling
Browse files Browse the repository at this point in the history
This moves the handling for custom CBOR tag types from cbor.Value to the
generic encoder/decoder to make them more widely usable.

Fixes #548
  • Loading branch information
agaffney committed Mar 17, 2024
1 parent 512417f commit 855da70
Show file tree
Hide file tree
Showing 8 changed files with 364 additions and 99 deletions.
16 changes: 1 addition & 15 deletions cbor/cbor.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023 Blink Labs Software
// Copyright 2024 Blink Labs Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -30,20 +30,6 @@ const (

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

// Useful tag numbers
CborTagCbor = 24
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
4 changes: 2 additions & 2 deletions cbor/decode.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023 Blink Labs Software
// Copyright 2024 Blink Labs Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -31,7 +31,7 @@ func Decode(dataBytes []byte, dest interface{}) (int, error) {
// This defaults to 32, but there are blocks in the wild using >64 nested levels
MaxNestedLevels: 256,
}
decMode, err := decOptions.DecMode()
decMode, err := decOptions.DecModeWithTags(customTagSet)
if err != nil {
return 0, err
}
Expand Down
4 changes: 2 additions & 2 deletions cbor/encode.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023 Blink Labs Software
// Copyright 2024 Blink Labs Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -25,7 +25,7 @@ import (

func Encode(data interface{}) ([]byte, error) {
buf := bytes.NewBuffer(nil)
em, err := _cbor.CoreDetEncOptions().EncMode()
em, err := _cbor.CoreDetEncOptions().EncModeWithTags(customTagSet)
if err != nil {
return nil, err
}
Expand Down
143 changes: 143 additions & 0 deletions cbor/tags.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
// Copyright 2024 Blink Labs Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cbor

import (
"math/big"
"reflect"

_cbor "github.com/fxamacker/cbor/v2"
)

const (
// Useful tag numbers
CborTagCbor = 24
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
)

var customTagSet _cbor.TagSet

func init() {
// Build custom tagset
customTagSet = _cbor.NewTagSet()
tagOpts := _cbor.TagOptions{EncTag: _cbor.EncTagRequired, DecTag: _cbor.DecTagRequired}
// Wrapped CBOR
if err := customTagSet.Add(
tagOpts,
reflect.TypeOf(WrappedCbor{}),
CborTagCbor,
); err != nil {
panic(err)
}
// Rational numbers
if err := customTagSet.Add(
tagOpts,
reflect.TypeOf(Rat{}),
CborTagRational,
); err != nil {
panic(err)
}
// Sets
if err := customTagSet.Add(
tagOpts,
reflect.TypeOf(Set{}),
CborTagSet,
); err != nil {
panic(err)
}
// Maps
if err := customTagSet.Add(
tagOpts,
reflect.TypeOf(Map{}),
CborTagMap,
); err != nil {
panic(err)
}
}

type WrappedCbor []byte

func (w *WrappedCbor) UnmarshalCBOR(cborData []byte) error {
var tmpData []byte
if _, err := Decode(cborData, &tmpData); err != nil {
return err
}
*w = WrappedCbor(tmpData[:])
return nil
}

func (w WrappedCbor) Bytes() []byte {
return w[:]
}

type Rat struct {
*big.Rat
}

func (r *Rat) UnmarshalCBOR(cborData []byte) error {
tmpRat := []int64{}
if _, err := Decode(cborData, &tmpRat); err != nil {
return err
}
r.Rat = big.NewRat(tmpRat[0], tmpRat[1])
return nil
}

func (r *Rat) MarshalCBOR() ([]byte, error) {
tmpData := _cbor.Tag{
Number: CborTagRational,
Content: []uint64{
r.Num().Uint64(),
r.Denom().Uint64(),
},
}
return Encode(&tmpData)
}

func (r *Rat) ToBigRat() *big.Rat {
return r.Rat
}

type Set []any

func (s *Set) UnmarshalCBOR(cborData []byte) error {
var tmpData []any
if _, err := Decode(cborData, &tmpData); err != nil {
return err
}
*s = tmpData[:]
return nil
}

type Map map[any]any

func (m *Map) UnmarshalCBOR(cborData []byte) error {
tmpData := make(map[any]any)
if _, err := Decode(cborData, &tmpData); err != nil {
return err
}
*m = tmpData
return nil
}
94 changes: 94 additions & 0 deletions cbor/tags_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Copyright 2023 Blink Labs Software
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cbor_test

import (
"encoding/hex"
"math/big"
"reflect"
"testing"

"github.com/blinklabs-io/gouroboros/cbor"
)

var tagsTestDefs = []struct {
cborHex string
object any
}{
{
cborHex: "d81843abcdef",
object: cbor.WrappedCbor([]byte{0xab, 0xcd, 0xef}),
},
{
cborHex: "d81e82031903e8",
object: cbor.Rat{
Rat: big.NewRat(3, 1000),
},
},
{
cborHex: "d9010283010203",
object: cbor.Set(
[]any{
uint64(1), uint64(2), uint64(3),
},
),
},
{
cborHex: "d90103a201020304",
object: cbor.Map(
map[any]any{
uint64(1): uint64(2),
uint64(3): uint64(4),
},
),
},
}

func TestTagsDecode(t *testing.T) {
for _, testDef := range tagsTestDefs {
cborData, err := hex.DecodeString(testDef.cborHex)
if err != nil {
t.Fatalf("failed to decode CBOR hex: %s", err)
}
var dest any
if _, err := cbor.Decode(cborData, &dest); err != nil {
t.Fatalf("failed to decode CBOR: %s", err)
}
if !reflect.DeepEqual(dest, testDef.object) {
t.Fatalf(
"CBOR did not decode to expected object\n got: %#v\n wanted: %#v",
dest,
testDef.object,
)
}
}
}

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

0 comments on commit 855da70

Please sign in to comment.