Skip to content

Commit

Permalink
feat: introduce UIntNode interface, use it exclusively for >int64
Browse files Browse the repository at this point in the history
  • Loading branch information
rvagg committed May 13, 2022
1 parent fd593b3 commit 1a2ecef
Show file tree
Hide file tree
Showing 8 changed files with 187 additions and 71 deletions.
26 changes: 12 additions & 14 deletions codec/dagcbor/marshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,6 @@ import (
// except for the `case datamodel.Kind_Link` block,
// which is dag-cbor's special sauce for schemafree links.

type uintNode interface {
UInt() uint64
Negative() bool
}

// EncodeOptions can be used to customize the behavior of an encoding function.
// The Encode method on this struct fits the codec.Encoder function interface.
type EncodeOptions struct {
Expand Down Expand Up @@ -105,25 +100,28 @@ func marshal(n datamodel.Node, tk *tok.Token, sink shared.TokenSink, options Enc
return err
case datamodel.Kind_Int:
var v uint64
var negative bool
if uin, ok := n.(uintNode); ok {
v = uin.UInt()
negative = uin.Negative()
var positive bool
if uin, ok := n.(datamodel.UintNode); ok {
var err error
v, positive, 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)
negative = sign == 1
positive = sign == 0
}
if negative {
tk.Type = tok.TInt
tk.Int = -int64(v)
} else {
if positive {
tk.Type = tok.TUint
tk.Uint = v
} else {
tk.Type = tok.TInt
tk.Int = -int64(v)
}
_, err := sink.Step(tk)
return err
Expand Down
73 changes: 73 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,73 @@ 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 rage of int64 type")

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

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))

// get uint form
uin, ok := n.(datamodel.UintNode)
qt.Assert(t, ok, qt.IsTrue)
val, positive, err := uin.AsUint()
qt.Assert(t, err, qt.IsNil)
qt.Assert(t, val, qt.Equals, uint64(math.MaxInt64))
qt.Assert(t, positive, qt.IsTrue)
})

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))

// get uint form
uin, ok := n.(datamodel.UintNode)
qt.Assert(t, ok, qt.IsTrue)
val, positive, err := uin.AsUint()
qt.Assert(t, err, qt.IsNil)
qt.Assert(t, val, qt.Equals, uint64(math.MaxInt64)+1)
qt.Assert(t, positive, qt.IsFalse)
})
}
5 changes: 4 additions & 1 deletion codec/dagcbor/unmarshal.go
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,10 @@ func unmarshal2(na datamodel.NodeAssembler, tokSrc shared.TokenSource, tk *tok.T
if *gas < 0 {
return ErrAllocationBudgetExceeded
}
return na.AssignNode(basicnode.NewUInt(tk.Uint, false))
if tk.Uint > uint64(math.MaxInt64) {
return na.AssignNode(basicnode.NewUInt(tk.Uint, true))
}
return na.AssignInt(int64(tk.Uint))
case tok.TFloat64:
*gas -= 1
if *gas < 0 {
Expand Down
27 changes: 0 additions & 27 deletions codec/dagcbor/unmarshal_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package dagcbor

import (
"bytes"
"encoding/hex"
"runtime"
"strings"
"testing"
Expand Down Expand Up @@ -74,28 +72,3 @@ func TestFunBlocks(t *testing.T) {
qt.Assert(t, err, qt.Equals, ErrTrailingBytes)
})
}

func TestInts(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
ii, err := n.AsInt()
qt.Assert(t, err, qt.IsNil)
qt.Assert(t, ii, qt.Equals, int64(-1)) // NOPE!

// get real, underlying value
uin, ok := n.(uintNode)
qt.Assert(t, ok, qt.IsTrue)
qt.Assert(t, uin.UInt(), qt.Equals, uint64(1<<64-1))
qt.Assert(t, uin.Negative(), qt.IsFalse)

var byts bytes.Buffer
err = Encode(n, &byts)
qt.Assert(t, err, qt.IsNil)
qt.Assert(t, hex.EncodeToString(byts.Bytes()), qt.Equals, "1bffffffffffffffff")
}
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 as well as a
// boolean to indicate whether the number is positive (true) or negative
// (false).
AsUint() (uint64, bool, 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
112 changes: 85 additions & 27 deletions node/basicnode/int.go
Original file line number Diff line number Diff line change
@@ -1,41 +1,41 @@
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, false}
_ datamodel.Node = plainInt(0)
_ datamodel.UintNode = plainInt(0)
_ datamodel.Node = uintNode{}
_ datamodel.UintNode = uintNode{}
_ datamodel.NodePrototype = Prototype__Int{}
_ datamodel.NodeBuilder = &plainInt__Builder{}
_ datamodel.NodeAssembler = &plainInt__Assembler{}
)

func newPlainInt(value int64) *plainInt {
sign := (value >> 63)
return &plainInt{
value: uint64((value ^ sign) - sign),
negative: sign == 1,
}
}

func NewInt(value int64) datamodel.Node {
return newPlainInt(value)
return plainInt(value)
}

func NewUInt(value uint64, negative bool) datamodel.Node {
v := plainInt{value, negative}
func NewUInt(value uint64, positive bool) datamodel.Node {
v := uintNode{value, positive}
return &v
}

// plainInt is a simple boxed int that complies with datamodel.Node.
type plainInt struct {
type uintNode struct {
value uint64
negative bool
positive bool
}

// -- Node interface methods -->
// plainInt is a simple boxed int that complies with datamodel.Node.
type plainInt int64

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

func (plainInt) Kind() datamodel.Kind {
return datamodel.Kind_Int
Expand Down Expand Up @@ -71,10 +71,7 @@ func (plainInt) AsBool() (bool, error) {
return mixins.Int{TypeName: "int"}.AsBool()
}
func (n plainInt) AsInt() (int64, error) {
if n.negative {
return -int64(n.value), nil
}
return int64(n.value), nil
return int64(n), nil
}
func (plainInt) AsFloat() (float64, error) {
return mixins.Int{TypeName: "int"}.AsFloat()
Expand All @@ -92,13 +89,74 @@ func (plainInt) Prototype() datamodel.NodePrototype {
return Prototype__Int{}
}

// special methods for plainInt to allow accessing the uint64 range
// allows plainInt to conform to the UintNode interface

func (n plainInt) AsUint() (uint64, bool, error) {
sign := (n >> 63)
return uint64((n ^ sign) - sign), sign == 0, nil
}

// -- Node interface methods for uintNode -->

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

// allows uintNode to conform to the UintNode interface

func (n uintNode) AsUint() (uint64, bool, error) {
return n.value, n.positive, nil
}

// -- NodePrototype -->
Expand Down Expand Up @@ -143,7 +201,7 @@ func (plainInt__Assembler) AssignBool(bool) error {
return mixins.IntAssembler{TypeName: "int"}.AssignBool(false)
}
func (na *plainInt__Assembler) AssignInt(v int64) error {
*na.w = *newPlainInt(v)
*na.w = plainInt(v)
return nil
}
func (plainInt__Assembler) AssignFloat(float64) error {
Expand All @@ -162,7 +220,7 @@ func (na *plainInt__Assembler) AssignNode(v datamodel.Node) error {
if v2, err := v.AsInt(); err != nil {
return err
} else {
*na.w = *newPlainInt(v2)
*na.w = plainInt(v2)
return nil
}
}
Expand Down
2 changes: 1 addition & 1 deletion node/basicnode/list.go
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ func (lva *plainList__ValueAssembler) AssignBool(v bool) error {
return lva.AssignNode(&vb)
}
func (lva *plainList__ValueAssembler) AssignInt(v int64) error {
vb := *newPlainInt(v)
vb := plainInt(v)
return lva.AssignNode(&vb)
}
func (lva *plainList__ValueAssembler) AssignFloat(v float64) error {
Expand Down
2 changes: 1 addition & 1 deletion node/basicnode/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -383,7 +383,7 @@ func (mva *plainMap__ValueAssembler) AssignBool(v bool) error {
return mva.AssignNode(&vb)
}
func (mva *plainMap__ValueAssembler) AssignInt(v int64) error {
vb := *newPlainInt(v)
vb := plainInt(v)
return mva.AssignNode(&vb)
}
func (mva *plainMap__ValueAssembler) AssignFloat(v float64) error {
Expand Down

0 comments on commit 1a2ecef

Please sign in to comment.