diff --git a/.travis.yml b/.travis.yml index bd3314c..36bfbd0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,15 +3,14 @@ sudo: false language: go go: - - 'tip' - -install: - - go get github.com/whyrusleeping/gx - - go get github.com/whyrusleeping/gx-go - - gx install --global + - 1.9.x + +before_install: + - make deps + script: - - gx test -v -race -coverprofile=coverage.txt -covermode=atomic . - + - bash <(curl -s https://raw.githubusercontent.com/ipfs/ci-helpers/master/travis-ci/run-standard-tests.sh) + after_success: - bash <(curl -s https://codecov.io/bash) @@ -20,5 +19,4 @@ cache: - $GOPATH/src/gx notifications: -email: false - + email: false diff --git a/encoding/cloner.go b/encoding/cloner.go new file mode 100644 index 0000000..0a04a3c --- /dev/null +++ b/encoding/cloner.go @@ -0,0 +1,33 @@ +package encoding + +import ( + "sync" + + refmt "github.com/polydawn/refmt" + "github.com/polydawn/refmt/obj/atlas" +) + +// PooledCloner is a thread-safe pooled object cloner. +type PooledCloner struct { + pool sync.Pool +} + +// NewPooledCloner returns a PooledCloner with the given atlas. Do not copy +// after use. +func NewPooledCloner(atl atlas.Atlas) PooledCloner { + return PooledCloner{ + pool: sync.Pool{ + New: func() interface{} { + return refmt.NewCloner(atl) + }, + }, + } +} + +// Clone clones a into b using a cloner from the pool. +func (p *PooledCloner) Clone(a, b interface{}) error { + c := p.pool.Get().(refmt.Cloner) + err := c.Clone(a, b) + p.pool.Put(c) + return err +} diff --git a/encoding/marshaller.go b/encoding/marshaller.go new file mode 100644 index 0000000..f4fe84b --- /dev/null +++ b/encoding/marshaller.go @@ -0,0 +1,82 @@ +package encoding + +import ( + "bytes" + "io" + "sync" + + cbor "github.com/polydawn/refmt/cbor" + "github.com/polydawn/refmt/obj/atlas" +) + +type proxyWriter struct { + w io.Writer +} + +func (w *proxyWriter) Write(b []byte) (int, error) { + return w.w.Write(b) +} + +// Marshaller is a reusbale CBOR marshaller. +type Marshaller struct { + marshal *cbor.Marshaller + writer proxyWriter +} + +// NewMarshallerAtlased constructs a new cbor Marshaller using the given atlas. +func NewMarshallerAtlased(atl atlas.Atlas) *Marshaller { + m := new(Marshaller) + m.marshal = cbor.NewMarshallerAtlased(&m.writer, atl) + return m +} + +// Encode encodes the given object to the given writer. +func (m *Marshaller) Encode(obj interface{}, w io.Writer) error { + m.writer.w = w + err := m.marshal.Marshal(obj) + m.writer.w = nil + return err +} + +// Marshal marshels the given object to a byte slice. +func (m *Marshaller) Marshal(obj interface{}) ([]byte, error) { + var buf bytes.Buffer + if err := m.Encode(obj, &buf); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// PooledMarshaller is a thread-safe pooled CBOR marshaller. +type PooledMarshaller struct { + pool sync.Pool +} + +// NewPooledMarshaller returns a PooledMarshaller with the given atlas. Do not +// copy after use. +func NewPooledMarshaller(atl atlas.Atlas) PooledMarshaller { + return PooledMarshaller{ + pool: sync.Pool{ + New: func() interface{} { + return NewMarshallerAtlased(atl) + }, + }, + } +} + +// Marshal marshals the passed object using the pool of marshallers. +func (p *PooledMarshaller) Marshal(obj interface{}) ([]byte, error) { + m := p.pool.Get().(*Marshaller) + bts, err := m.Marshal(obj) + p.pool.Put(m) + return bts, err +} + +// Encode encodes the passed object to the given writer using the pool of +// marshallers. +func (p *PooledMarshaller) Encode(obj interface{}, w io.Writer) error { + m := p.pool.Get().(*Marshaller) + err := m.Encode(obj, w) + p.pool.Put(m) + return err +} diff --git a/encoding/unmarshaller.go b/encoding/unmarshaller.go new file mode 100644 index 0000000..0ad2e39 --- /dev/null +++ b/encoding/unmarshaller.go @@ -0,0 +1,79 @@ +package encoding + +import ( + "bytes" + "io" + "sync" + + cbor "github.com/polydawn/refmt/cbor" + "github.com/polydawn/refmt/obj/atlas" +) + +type proxyReader struct { + r io.Reader +} + +func (r *proxyReader) Read(b []byte) (int, error) { + return r.r.Read(b) +} + +// Unmarshaller is a reusable CBOR unmarshaller. +type Unmarshaller struct { + unmarshal *cbor.Unmarshaller + reader proxyReader +} + +// NewUnmarshallerAtlased creates a new reusable unmarshaller. +func NewUnmarshallerAtlased(atl atlas.Atlas) *Unmarshaller { + m := new(Unmarshaller) + m.unmarshal = cbor.NewUnmarshallerAtlased(&m.reader, atl) + return m +} + +// Decode reads a CBOR object from the given reader and decodes it into the +// given object. +func (m *Unmarshaller) Decode(r io.Reader, obj interface{}) error { + m.reader.r = r + err := m.unmarshal.Unmarshal(obj) + m.reader.r = nil + return err +} + +// Unmarshal unmarshals the given CBOR byte slice into the given object. +func (m *Unmarshaller) Unmarshal(b []byte, obj interface{}) error { + return m.Decode(bytes.NewReader(b), obj) +} + +// PooledUnmarshaller is a thread-safe pooled CBOR unmarshaller. +type PooledUnmarshaller struct { + pool sync.Pool +} + +// NewPooledUnmarshaller returns a PooledUnmarshaller with the given atlas. Do +// not copy after use. +func NewPooledUnmarshaller(atl atlas.Atlas) PooledUnmarshaller { + return PooledUnmarshaller{ + pool: sync.Pool{ + New: func() interface{} { + return NewUnmarshallerAtlased(atl) + }, + }, + } +} + +// Decode decodes an object from the passed reader into the given object using +// the pool of unmarshallers. +func (p *PooledUnmarshaller) Decode(r io.Reader, obj interface{}) error { + u := p.pool.Get().(*Unmarshaller) + err := u.Decode(r, obj) + p.pool.Put(u) + return err +} + +// Unmarshal unmarshals the passed object using the pool of unmarshallers. +func (p *PooledUnmarshaller) Unmarshal(b []byte, obj interface{}) error { + u := p.pool.Get().(*Unmarshaller) + err := u.Unmarshal(b, obj) + p.pool.Put(u) + return err +} diff --git a/node.go b/node.go index 6f672dd..abcf745 100644 --- a/node.go +++ b/node.go @@ -1,7 +1,6 @@ package cbornode import ( - "bytes" "encoding/json" "errors" "fmt" @@ -14,12 +13,37 @@ import ( cid "github.com/ipfs/go-cid" node "github.com/ipfs/go-ipld-format" mh "github.com/multiformats/go-multihash" - cbor "github.com/whyrusleeping/cbor/go" ) +// CBORTagLink is the integer used to represent tags in CBOR. const CBORTagLink = 42 -// Decode a CBOR encoded Block into an IPLD Node. +// Node represents an IPLD node. +type Node struct { + obj interface{} + tree []string + links []*node.Link + raw []byte + cid cid.Cid +} + +// Compile time check to make sure Node implements the node.Node interface +var _ node.Node = (*Node)(nil) + +var ( + // ErrNoSuchLink is returned when no link with the given name was found. + ErrNoSuchLink = errors.New("no such link found") + ErrNonLink = errors.New("non-link found at given path") + ErrInvalidLink = errors.New("link value should have been bytes") + ErrInvalidKeys = errors.New("map keys must be strings") + ErrArrayOutOfRange = errors.New("array index out of range") + ErrNoLinks = errors.New("tried to resolve through object that had no links") + ErrEmptyLink = errors.New("link value was empty") + ErrInvalidMultibase = errors.New("invalid multibase on IPLD link") + ErrNonStringLink = errors.New("link should have been a string") +) + +// DecodeBlock decodes a CBOR encoded Block into an IPLD Node. // // This method *does not* canonicalize and *will* preserve the CID. As a matter // of fact, it will assume that `block.Cid()` returns the correct CID and will @@ -36,10 +60,14 @@ func DecodeBlock(block blocks.Block) (node.Node, error) { } func decodeBlock(block blocks.Block) (*Node, error) { - m, err := decodeCBOR(block.RawData()) - if err != nil { + var m interface{} + if err := DecodeInto(block.RawData(), &m); err != nil { return nil, err } + return newObject(block, m) +} + +func newObject(block blocks.Block, m interface{}) (*Node, error) { tree, err := compTree(m) if err != nil { return nil, err @@ -48,6 +76,7 @@ func decodeBlock(block blocks.Block) (*Node, error) { if err != nil { return nil, err } + return &Node{ obj: m, tree: tree, @@ -59,7 +88,7 @@ func decodeBlock(block blocks.Block) (*Node, error) { var _ node.DecodeBlockFunc = DecodeBlock -// Decode a CBOR object into an IPLD Node. +// Decode decodes a CBOR object into an IPLD Node. // // If passed a non-canonical CBOR node, this function will canonicalize it. // Therefore, `bytes.Equal(b, Decode(b).RawData())` may not hold. If you already @@ -69,65 +98,34 @@ var _ node.DecodeBlockFunc = DecodeBlock // // Note: This function does not hold onto `b`. You may reuse it. func Decode(b []byte, mhType uint64, mhLen int) (*Node, error) { - m, err := decodeCBOR(b) - if err != nil { + var m interface{} + if err := DecodeInto(b, &m); err != nil { return nil, err } + // We throw away `b` here to ensure that we canonicalize the encoded // CBOR object. return WrapObject(m, mhType, mhLen) } -// DecodeInto decodes a serialized ipld cbor object into the given object. +// DecodeInto decodes a serialized IPLD cbor object into the given object. func DecodeInto(b []byte, v interface{}) error { - // The cbor library really doesnt make this sort of operation easy on us - m, err := decodeCBOR(b) - if err != nil { - return err - } - - jsonable, err := convertToJsonIsh(m) - if err != nil { - return err - } + return unmarshaller.Unmarshal(b, v) +} - jsonb, err := json.Marshal(jsonable) +// WrapObject converts an arbitrary object into a Node. +func WrapObject(m interface{}, mhType uint64, mhLen int) (*Node, error) { + data, err := marshaller.Marshal(m) if err != nil { - return err + return nil, err } - return json.Unmarshal(jsonb, v) - -} - -// Decodes a cbor node into an object. -func decodeCBOR(b []byte) (m interface{}, err error) { - defer func() { - if r := recover(); r != nil { - err = fmt.Errorf("cbor panic: %s", r) - } - }() - dec := cbor.NewDecoder(bytes.NewReader(b)) - dec.TagDecoders[CBORTagLink] = new(IpldLinkDecoder) - err = dec.Decode(&m) - return -} - -var ErrNoSuchLink = errors.New("no such link found") - -type Node struct { - obj interface{} - tree []string - links []*node.Link - raw []byte - cid cid.Cid -} - -func WrapObject(m interface{}, mhType uint64, mhLen int) (*Node, error) { - data, err := DumpObject(m) + var obj interface{} + err = cloner.Clone(m, &obj) if err != nil { return nil, err } + if mhType == math.MaxUint64 { mhType = mh.SHA2_256 } @@ -143,15 +141,23 @@ func WrapObject(m interface{}, mhType uint64, mhLen int) (*Node, error) { // TODO: Shouldn't this just panic? return nil, err } - // Do not reuse `m`. We need to re-decode it to put it in the right - // form. - return decodeBlock(block) + // No need to deserialize. We can just deep copy. + return newObject(block, obj) } +// Resolve resolves a given path, and returns the object found at the end, as well +// as the possible tail of the path that was not resolved. func (n *Node) Resolve(path []string) (interface{}, []string, error) { var cur interface{} = n.obj for i, val := range path { switch curv := cur.(type) { + case map[string]interface{}: + next, ok := curv[val] + if !ok { + return nil, nil, ErrNoSuchLink + } + + cur = next case map[interface{}]interface{}: next, ok := curv[val] if !ok { @@ -166,14 +172,14 @@ func (n *Node) Resolve(path []string) (interface{}, []string, error) { } if n < 0 || n >= len(curv) { - return nil, nil, fmt.Errorf("array index out of range") + return nil, nil, ErrArrayOutOfRange } cur = curv[n] case cid.Cid: return &node.Link{Cid: curv}, path[i:], nil default: - return nil, nil, errors.New("tried to resolve through object that had no links") + return nil, nil, ErrNoLinks } } @@ -182,7 +188,7 @@ func (n *Node) Resolve(path []string) (interface{}, []string, error) { return &node.Link{Cid: lnk}, nil, nil } - jsonish, err := convertToJsonIsh(cur) + jsonish, err := convertToJSONIsh(cur) if err != nil { return nil, nil, err } @@ -190,6 +196,7 @@ func (n *Node) Resolve(path []string) (interface{}, []string, error) { return jsonish, nil, nil } +// Copy creates a copy of the Node. func (n *Node) Copy() node.Node { links := make([]*node.Link, len(n.links)) copy(links, n.links) @@ -211,6 +218,12 @@ func (n *Node) Copy() node.Node { func copyObj(i interface{}) interface{} { switch i := i.(type) { + case map[string]interface{}: + out := make(map[string]interface{}) + for k, v := range i { + out[k] = copyObj(v) + } + return out case map[interface{}]interface{}: out := make(map[interface{}]interface{}) for k, v := range i { @@ -224,12 +237,15 @@ func copyObj(i interface{}) interface{} { } return out default: + // TODO: do not be lazy // being lazy for now // use caution return i } } +// ResolveLink resolves a path and returns the raw Link at the end, as well as +// the possible tail of the path that was not resolved. func (n *Node) ResolveLink(path []string) (*node.Link, []string, error) { obj, rest, err := n.Resolve(path) if err != nil { @@ -237,27 +253,14 @@ func (n *Node) ResolveLink(path []string) (*node.Link, []string, error) { } lnk, ok := obj.(*node.Link) - if ok { - return lnk, rest, nil - } - - return nil, rest, fmt.Errorf("found non-link at given path") -} - -func linkCast(lnk interface{}) (*node.Link, error) { - lnkb, ok := lnk.([]byte) if !ok { - return nil, errors.New("incorrectly formatted link") - } - - c, err := cid.Cast(lnkb) - if err != nil { - return nil, err + return nil, rest, ErrNonLink } - return &node.Link{Cid: c}, nil + return lnk, rest, nil } +// Tree returns a flattend array of paths at the given path for the given depth. func (n *Node) Tree(path string, depth int) []string { if path == "" && depth == -1 { return n.tree @@ -302,6 +305,7 @@ func compTree(obj interface{}) ([]string, error) { return out, nil } +// Links lists all known links of the Node. func (n *Node) Links() []*node.Link { return n.links } @@ -317,6 +321,7 @@ func compLinks(obj interface{}) ([]*node.Link, error) { if err != nil { return nil, err } + return out, nil } @@ -326,6 +331,14 @@ func traverse(obj interface{}, cur string, cb func(string, interface{}) error) e } switch obj := obj.(type) { + case map[string]interface{}: + for k, v := range obj { + this := cur + "/" + k + if err := traverse(v, this, cb); err != nil { + return err + } + } + return nil case map[interface{}]interface{}: for k, v := range obj { ks, ok := k.(string) @@ -351,26 +364,17 @@ func traverse(obj interface{}, cur string, cb func(string, interface{}) error) e } } +// RawData returns the raw bytes that represent the Node as serialized CBOR. func (n *Node) RawData() []byte { return n.raw } -func DumpObject(obj interface{}) ([]byte, error) { - buf := new(bytes.Buffer) - enc := cbor.NewEncoder(buf) - enc.SetFilter(EncoderFilter) - err := enc.Encode(obj) - if err != nil { - return nil, err - } - - return buf.Bytes(), nil -} - +// Cid returns the canonical Cid of the NOde. func (n *Node) Cid() cid.Cid { return n.cid } +// Loggable returns a loggable representation of the Node. func (n *Node) Loggable() map[string]interface{} { return map[string]interface{}{ "node_type": "cbor", @@ -378,20 +382,25 @@ func (n *Node) Loggable() map[string]interface{} { } } +// Size returns the size of the binary representation of the Node. func (n *Node) Size() (uint64, error) { return uint64(len(n.RawData())), nil } +// Stat returns stats about the Node. +// TODO: implement? func (n *Node) Stat() (*node.NodeStat, error) { return &node.NodeStat{}, nil } +// String returns the string representation of the CID of the Node. func (n *Node) String() string { return n.Cid().String() } +// MarshalJSON converts the Node into its JSON representation. func (n *Node) MarshalJSON() ([]byte, error) { - out, err := convertToJsonIsh(n.obj) + out, err := convertToJSONIsh(n.obj) if err != nil { return nil, err } @@ -399,11 +408,17 @@ func (n *Node) MarshalJSON() ([]byte, error) { return json.Marshal(out) } +// DumpObject marshals any object into its CBOR serialized byte representation +// TODO: rename +func DumpObject(obj interface{}) (out []byte, err error) { + return marshaller.Marshal(obj) +} + func toSaneMap(n map[interface{}]interface{}) (interface{}, error) { if lnk, ok := n["/"]; ok && len(n) == 1 { lnkb, ok := lnk.([]byte) if !ok { - return nil, fmt.Errorf("link value should have been bytes") + return nil, ErrInvalidLink } c, err := cid.Cast(lnkb) @@ -417,10 +432,10 @@ func toSaneMap(n map[interface{}]interface{}) (interface{}, error) { for k, v := range n { ks, ok := k.(string) if !ok { - return nil, fmt.Errorf("map keys must be strings") + return nil, ErrInvalidKeys } - obj, err := convertToJsonIsh(v) + obj, err := convertToJSONIsh(v) if err != nil { return nil, err } @@ -431,7 +446,7 @@ func toSaneMap(n map[interface{}]interface{}) (interface{}, error) { return out, nil } -func convertToJsonIsh(v interface{}) (interface{}, error) { +func convertToJSONIsh(v interface{}) (interface{}, error) { switch v := v.(type) { case map[interface{}]interface{}: return toSaneMap(v) @@ -441,7 +456,7 @@ func convertToJsonIsh(v interface{}) (interface{}, error) { return []interface{}{}, nil } for _, i := range v { - obj, err := convertToJsonIsh(i) + obj, err := convertToJSONIsh(i) if err != nil { return nil, err } @@ -454,7 +469,8 @@ func convertToJsonIsh(v interface{}) (interface{}, error) { } } -func FromJson(r io.Reader, mhType uint64, mhLen int) (*Node, error) { +// FromJSON converts incoming JSON into a Node. +func FromJSON(r io.Reader, mhType uint64, mhLen int) (*Node, error) { var m interface{} err := json.NewDecoder(r).Decode(&m) if err != nil { @@ -469,34 +485,37 @@ func FromJson(r io.Reader, mhType uint64, mhLen int) (*Node, error) { return WrapObject(obj, mhType, mhLen) } -func convertMapSIToCbor(from map[string]interface{}) (map[interface{}]interface{}, error) { - to := make(map[interface{}]interface{}) - for k, v := range from { - out, err := convertToCborIshObj(v) - if err != nil { - return nil, err - } - to[k] = out - } - - return to, nil -} - func convertToCborIshObj(i interface{}) (interface{}, error) { switch v := i.(type) { case map[string]interface{}: + if len(v) == 0 && v != nil { + return v, nil + } + if lnk, ok := v["/"]; ok && len(v) == 1 { // special case for links vstr, ok := lnk.(string) if !ok { - return nil, fmt.Errorf("link should have been a string") + return nil, ErrNonStringLink } return cid.Decode(vstr) } - return convertMapSIToCbor(v) + for a, b := range v { + val, err := convertToCborIshObj(b) + if err != nil { + return nil, err + } + + v[a] = val + } + return v, nil case []interface{}: + if len(v) == 0 && v != nil { + return v, nil + } + var out []interface{} for _, o := range v { obj, err := convertToCborIshObj(o) @@ -513,53 +532,25 @@ func convertToCborIshObj(i interface{}) (interface{}, error) { } } -func EncoderFilter(i interface{}) interface{} { - link, ok := i.(cid.Cid) - if !ok { - return i - } - - return &cbor.CBORTag{ - Tag: CBORTagLink, - WrappedObject: append([]byte{0}, link.Bytes()...), // TODO: manually doing binary multibase - } -} - -type IpldLinkDecoder struct{} - -func (d *IpldLinkDecoder) DecodeTarget() interface{} { - return &[]byte{} -} - -func (d *IpldLinkDecoder) GetTag() uint64 { - return CBORTagLink -} - -func (d *IpldLinkDecoder) PostDecode(i interface{}) (interface{}, error) { - ibarr, ok := i.(*[]byte) - if !ok { - return nil, fmt.Errorf("expected a byte array in IpldLink PostDecode") - } - - barr := *ibarr - - if len(barr) == 0 { - return nil, fmt.Errorf("link value was empty") +func castBytesToCid(x []byte) (cid.Cid, error) { + if len(x) == 0 { + return cid.Cid{}, ErrEmptyLink } // TODO: manually doing multibase checking here since our deps don't // support binary multibase yet - if barr[0] != 0 { - return nil, fmt.Errorf("invalid multibase on ipld link") + if x[0] != 0 { + return cid.Cid{}, ErrInvalidMultibase } - c, err := cid.Cast(barr[1:]) + c, err := cid.Cast(x[1:]) if err != nil { - return nil, err + return cid.Cid{}, ErrInvalidLink } return c, nil } -var _ cbor.TagDecoder = (*IpldLinkDecoder)(nil) -var _ node.Node = (*Node)(nil) +func castCidToBytes(link cid.Cid) ([]byte, error) { + return append([]byte{0}, link.Bytes()...), nil +} diff --git a/node_test.go b/node_test.go index a1cea65..7738b59 100644 --- a/node_test.go +++ b/node_test.go @@ -2,9 +2,12 @@ package cbornode import ( "bytes" + "encoding/hex" "encoding/json" "fmt" "io/ioutil" + "math" + "math/big" "sort" "strings" "testing" @@ -53,6 +56,7 @@ func TestDecodeInto(t *testing.T) { if err != nil { t.Fatal(err) } + if len(m) != 1 || m["name"] != "foo" { t.Fatal("failed to decode object") } @@ -80,7 +84,7 @@ func TestBasicMarshal(t *testing.T) { "name": "foo", "bar": c, } - + fmt.Printf("cid: %s\n", c.String()) nd, err := WrapObject(obj, mh.SHA2_256, -1) if err != nil { t.Fatal(err) @@ -95,6 +99,9 @@ func TestBasicMarshal(t *testing.T) { t.Fatal(err) } + fmt.Printf("before %v\n", nd.RawData()) + fmt.Printf("after %v\n", back.RawData()) + if err := assertCid(back.Cid(), "zdpuApUZEHofKXuTs2Yv2CLBeiASQrc9FojFLSZWcyZq6dZhb"); err != nil { t.Fatal(err) } @@ -118,8 +125,9 @@ func TestMarshalRoundtrip(t *testing.T) { c2 := cid.NewCidV0(u.Hash([]byte("something2"))) c3 := cid.NewCidV0(u.Hash([]byte("something3"))) - obj := map[interface{}]interface{}{ - "foo": "bar", + obj := map[string]interface{}{ + "foo": "bar", + "hello": c1, "baz": []interface{}{ c1, c2, @@ -134,11 +142,19 @@ func TestMarshalRoundtrip(t *testing.T) { t.Fatal(err) } - if err := assertCid(nd1.Cid(), "zdpuAwc5bPhfHGdA4rs3qKzr3QB3Fjp3D16C8BRRyWzTPpY9R"); err != nil { - t.Fatal(err) + if err := assertCid(nd1.Cid(), "zdpuAo2h1rUzWW3EPm1WBaLhTcq7G3RoXk2o7rqD1qm4jdzrE"); err != nil { + orig, err1 := json.Marshal(obj) + if err1 != nil { + t.Fatal(err1) + } + js, err1 := nd1.MarshalJSON() + if err1 != nil { + t.Fatal(err1) + } + t.Fatalf("%s != %s\n%s", orig, js, err) } - if len(nd1.Links()) != 3 { + if len(nd1.Links()) != 4 { t.Fatal("didnt have enough links") } @@ -196,13 +212,13 @@ func TestTree(t *testing.T) { obj := map[string]interface{}{ "foo": c1, "baz": []interface{}{c2, c3, "c"}, - "cats": map[interface{}]interface{}{ + "cats": map[string]interface{}{ "qux": map[string]interface{}{ "boo": 1, "baa": c4, "bee": 3, "bii": 4, - "buu": map[interface{}]string{ + "buu": map[string]string{ "coat": "rain", }, }, @@ -258,6 +274,15 @@ func TestTree(t *testing.T) { } func TestParsing(t *testing.T) { + // This shouldn't pass + // Debug representation from cbor.io is + // + // D9 0102 # tag(258) + // 58 25 # bytes(37) + // A503221220659650FC3443C916428048EFC5BA4558DC863594980A59F5CB3C4D84867E6D31 # "\xA5\x03\"\x12 e\x96P\xFC4C\xC9\x16B\x80H\xEF\xC5\xBAEX\xDC\x865\x94\x98\nY\xF5\xCB