Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bindnode): support full uint64 range #429

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions codec/dagcbor/marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,31 @@ func marshal(n datamodel.Node, tk *tok.Token, sink shared.TokenSink, options Enc
_, err = sink.Step(tk)
return err
case datamodel.Kind_Int:
v, err := n.AsInt()
if err != nil {
return err
var v uint64
positive := true
if uin, ok := n.(datamodel.UintNode); ok {
var err error
v, err = uin.AsUint()
if err != nil {
return err
}
} else {
i, err := n.AsInt()
if err != nil {
return err
}
sign := (i >> 63)
v = uint64((i ^ sign) - sign)
positive = sign == 0
}
tk.Type = tok.TInt
tk.Int = int64(v)
_, err = sink.Step(tk)
if positive {
tk.Type = tok.TUint
tk.Uint = v
} else {
tk.Type = tok.TInt
tk.Int = -int64(v)
}
_, err := sink.Step(tk)
return err
case datamodel.Kind_Float:
v, err := n.AsFloat()
Expand Down
64 changes: 64 additions & 0 deletions codec/dagcbor/roundtrip_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@ package dagcbor
import (
"bytes"
"crypto/rand"
"encoding/hex"
"math"
"strings"
"testing"

qt "github.com/frankban/quicktest"
cid "github.com/ipfs/go-cid"

"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/fluent"
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
"github.com/ipld/go-ipld-prime/node/basicnode"
Expand Down Expand Up @@ -115,3 +118,64 @@ func TestRoundtripLinksAndBytes(t *testing.T) {
reconstructed := nb.Build()
qt.Check(t, reconstructed, nodetests.NodeContentEquals, linkByteNode)
}

func TestInts(t *testing.T) {
t.Run("max uint64", func(t *testing.T) {
buf, err := hex.DecodeString("1bffffffffffffffff") // max uint64
qt.Assert(t, err, qt.IsNil)
nb := basicnode.Prototype.Any.NewBuilder()
err = Decode(nb, bytes.NewReader(buf))
qt.Assert(t, err, qt.IsNil)
n := nb.Build()

// the overflowed AsInt() int64 cast
_, err = n.AsInt()
qt.Assert(t, err.Error(), qt.Equals, "unsigned integer out of range of int64 type")

// get real, underlying value
uin, ok := n.(datamodel.UintNode)
qt.Assert(t, ok, qt.IsTrue)
val, err := uin.AsUint()
qt.Assert(t, err, qt.IsNil)
qt.Assert(t, val, qt.Equals, uint64(math.MaxUint64))

var byts bytes.Buffer
err = Encode(n, &byts)
qt.Assert(t, err, qt.IsNil)
qt.Assert(t, hex.EncodeToString(byts.Bytes()), qt.Equals, "1bffffffffffffffff")
})

t.Run("max int64", func(t *testing.T) {
buf, err := hex.DecodeString("1b7fffffffffffffff") // max int64
qt.Assert(t, err, qt.IsNil)
nb := basicnode.Prototype.Any.NewBuilder()
err = Decode(nb, bytes.NewReader(buf))
qt.Assert(t, err, qt.IsNil)
n := nb.Build()

ii, err := n.AsInt()
qt.Assert(t, err, qt.IsNil)
qt.Assert(t, ii, qt.Equals, int64(math.MaxInt64))

// doesn't need to be a uint
_, ok := n.(datamodel.UintNode)
qt.Assert(t, ok, qt.IsFalse)
})

t.Run("min int64", func(t *testing.T) {
buf, err := hex.DecodeString("3b7fffffffffffffff") // min int64
qt.Assert(t, err, qt.IsNil)
nb := basicnode.Prototype.Any.NewBuilder()
err = Decode(nb, bytes.NewReader(buf))
qt.Assert(t, err, qt.IsNil)
n := nb.Build()

ii, err := n.AsInt()
qt.Assert(t, err, qt.IsNil)
qt.Assert(t, ii, qt.Equals, int64(math.MinInt64))

// doesn't need to be a uint
_, ok := n.(datamodel.UintNode)
qt.Assert(t, ok, qt.IsFalse)
})
}
6 changes: 5 additions & 1 deletion codec/dagcbor/unmarshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

"github.com/ipld/go-ipld-prime/datamodel"
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
"github.com/ipld/go-ipld-prime/node/basicnode"
)

var (
Expand Down Expand Up @@ -275,7 +276,10 @@ func unmarshal2(na datamodel.NodeAssembler, tokSrc shared.TokenSource, tk *tok.T
if *gas < 0 {
return ErrAllocationBudgetExceeded
}
return na.AssignInt(int64(tk.Uint)) // FIXME overflow check
if tk.Uint > uint64(math.MaxInt64) {
return na.AssignNode(basicnode.NewUInt(tk.Uint))
}
return na.AssignInt(int64(tk.Uint))
case tok.TFloat64:
*gas -= 1
if *gas < 0 {
Expand Down
11 changes: 11 additions & 0 deletions datamodel/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,17 @@ type Node interface {
Prototype() NodePrototype
}

// UintNode is an optional interface that can be used to represent an Int node
// that provides access to the full uint64 range.
type UintNode interface {
Node

// AsUint returns a uint64 representing the underlying integer if possible.
// This may return an error if the Node represents a negative integer that
// cannot be represented as a uint64.
AsUint() (uint64, error)
}

// LargeBytesNode is an optional interface extending a Bytes node that allows its
// contents to be accessed through an io.ReadSeeker instead of a []byte slice. Use of
// an io.Reader is encouraged, as it allows for streaming large byte slices
Expand Down
82 changes: 79 additions & 3 deletions node/basicnode/int.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
package basicnode

import (
"fmt"
"math"

"github.com/ipld/go-ipld-prime/datamodel"
"github.com/ipld/go-ipld-prime/node/mixins"
)

var (
_ datamodel.Node = plainInt(0)
_ datamodel.Node = plainUint(0)
_ datamodel.UintNode = plainUint(0)
_ datamodel.NodePrototype = Prototype__Int{}
_ datamodel.NodeBuilder = &plainInt__Builder{}
_ datamodel.NodeAssembler = &plainInt__Assembler{}
)

func NewInt(value int64) datamodel.Node {
v := plainInt(value)
return &v
return plainInt(value)
}

func NewUInt(value uint64) datamodel.Node {
return plainUint(value)
}

// plainInt is a simple boxed int that complies with datamodel.Node.
type plainInt int64

// -- Node interface methods -->
// -- Node interface methods for plainInt -->

func (plainInt) Kind() datamodel.Kind {
return datamodel.Kind_Int
Expand Down Expand Up @@ -74,6 +82,74 @@ func (plainInt) Prototype() datamodel.NodePrototype {
return Prototype__Int{}
}

// plainUint is a simple boxed uint64 that complies with datamodel.Node,
// allowing representation of the uint64 range above the int64 maximum via the
// UintNode interface
type plainUint uint64

// -- Node interface methods for plainUint -->

func (plainUint) Kind() datamodel.Kind {
return datamodel.Kind_Int
}
func (plainUint) LookupByString(string) (datamodel.Node, error) {
return mixins.Int{TypeName: "int"}.LookupByString("")
}
func (plainUint) LookupByNode(key datamodel.Node) (datamodel.Node, error) {
return mixins.Int{TypeName: "int"}.LookupByNode(nil)
}
func (plainUint) LookupByIndex(idx int64) (datamodel.Node, error) {
return mixins.Int{TypeName: "int"}.LookupByIndex(0)
}
func (plainUint) LookupBySegment(seg datamodel.PathSegment) (datamodel.Node, error) {
return mixins.Int{TypeName: "int"}.LookupBySegment(seg)
}
func (plainUint) MapIterator() datamodel.MapIterator {
return nil
}
func (plainUint) ListIterator() datamodel.ListIterator {
return nil
}
func (plainUint) Length() int64 {
return -1
}
func (plainUint) IsAbsent() bool {
return false
}
func (plainUint) IsNull() bool {
return false
}
func (plainUint) AsBool() (bool, error) {
return mixins.Int{TypeName: "int"}.AsBool()
}
func (n plainUint) AsInt() (int64, error) {
if uint64(n) > uint64(math.MaxInt64) {
return -1, fmt.Errorf("unsigned integer out of range of int64 type")
}
return int64(n), nil
}
func (plainUint) AsFloat() (float64, error) {
return mixins.Int{TypeName: "int"}.AsFloat()
}
func (plainUint) AsString() (string, error) {
return mixins.Int{TypeName: "int"}.AsString()
}
func (plainUint) AsBytes() ([]byte, error) {
return mixins.Int{TypeName: "int"}.AsBytes()
}
func (plainUint) AsLink() (datamodel.Link, error) {
return mixins.Int{TypeName: "int"}.AsLink()
}
func (plainUint) Prototype() datamodel.NodePrototype {
return Prototype__Int{}
}

// allows plainUint to conform to the plainUint interface

func (n plainUint) AsUint() (uint64, error) {
return uint64(n), nil
}

// -- NodePrototype -->

type Prototype__Int struct{}
Expand Down
2 changes: 1 addition & 1 deletion node/bindnode/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,7 +301,7 @@ func Wrap(ptrVal interface{}, schemaType schema.Type, options ...Option) schema.
// inferred.
verifyCompatibility(cfg, make(map[seenEntry]bool), goVal.Type(), schemaType)
}
return &_node{cfg: cfg, val: goVal, schemaType: schemaType}
return newNode(cfg, schemaType, goVal)
}

// TODO: consider making our own Node interface, like:
Expand Down
87 changes: 87 additions & 0 deletions node/bindnode/api_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package bindnode_test

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

"github.com/ipld/go-ipld-prime"
"github.com/ipld/go-ipld-prime/codec/dagcbor"
"github.com/ipld/go-ipld-prime/node/bindnode"

qt "github.com/frankban/quicktest"
)

func TestUint64Struct(t *testing.T) {
t.Run("in struct", func(t *testing.T) {
type IntHolder struct {
Int32 int32
Int64 int64
Uint64 uint64
}
schema := `
type IntHolder struct {
Int32 Int
Int64 Int
Uint64 Int
}
`

maxExpectedHex := "a365496e7433321a7fffffff65496e7436341b7fffffffffffffff6655696e7436341bffffffffffffffff"
maxExpected, err := hex.DecodeString(maxExpectedHex)
qt.Assert(t, err, qt.IsNil)

typeSystem, err := ipld.LoadSchemaBytes([]byte(schema))
qt.Assert(t, err, qt.IsNil)
schemaType := typeSystem.TypeByName("IntHolder")
proto := bindnode.Prototype(&IntHolder{}, schemaType)

node, err := ipld.DecodeUsingPrototype([]byte(maxExpected), dagcbor.Decode, proto)
qt.Assert(t, err, qt.IsNil)

typ := bindnode.Unwrap(node)
inst, ok := typ.(*IntHolder)
qt.Assert(t, ok, qt.IsTrue)

qt.Assert(t, *inst, qt.DeepEquals, IntHolder{
Int32: math.MaxInt32,
Int64: math.MaxInt64,
Uint64: math.MaxUint64,
})

node = bindnode.Wrap(inst, schemaType).Representation()
byt, err := ipld.Encode(node, dagcbor.Encode)
qt.Assert(t, err, qt.IsNil)

qt.Assert(t, hex.EncodeToString(byt), qt.Equals, maxExpectedHex)
})

t.Run("plain", func(t *testing.T) {
type IntHolder uint64
schema := `type IntHolder int`

maxExpectedHex := "1bffffffffffffffff"
maxExpected, err := hex.DecodeString(maxExpectedHex)
qt.Assert(t, err, qt.IsNil)

typeSystem, err := ipld.LoadSchemaBytes([]byte(schema))
qt.Assert(t, err, qt.IsNil)
schemaType := typeSystem.TypeByName("IntHolder")
proto := bindnode.Prototype((*IntHolder)(nil), schemaType)

node, err := ipld.DecodeUsingPrototype([]byte(maxExpected), dagcbor.Decode, proto)
qt.Assert(t, err, qt.IsNil)

typ := bindnode.Unwrap(node)
inst, ok := typ.(*IntHolder)
qt.Assert(t, ok, qt.IsTrue)

qt.Assert(t, *inst, qt.Equals, IntHolder(math.MaxUint64))

node = bindnode.Wrap(inst, schemaType).Representation()
byt, err := ipld.Encode(node, dagcbor.Encode)
qt.Assert(t, err, qt.IsNil)

qt.Assert(t, hex.EncodeToString(byt), qt.Equals, maxExpectedHex)
})
}
Loading