Skip to content

Commit e424b72

Browse files
committed
feat: introduce UIntNode interface, used within DAG-CBOR codec
1 parent 120991f commit e424b72

File tree

5 files changed

+187
-10
lines changed

5 files changed

+187
-10
lines changed

codec/dagcbor/marshal.go

+15-6
Original file line numberDiff line numberDiff line change
@@ -99,13 +99,22 @@ func marshal(n datamodel.Node, tk *tok.Token, sink shared.TokenSink, options Enc
9999
_, err = sink.Step(tk)
100100
return err
101101
case datamodel.Kind_Int:
102-
v, err := n.AsInt()
103-
if err != nil {
104-
return err
102+
if uin, ok := n.(datamodel.UintNode); ok {
103+
v, err := uin.AsUint()
104+
if err != nil {
105+
return err
106+
}
107+
tk.Type = tok.TUint
108+
tk.Uint = v
109+
} else {
110+
v, err := n.AsInt()
111+
if err != nil {
112+
return err
113+
}
114+
tk.Type = tok.TInt
115+
tk.Int = v
105116
}
106-
tk.Type = tok.TInt
107-
tk.Int = int64(v)
108-
_, err = sink.Step(tk)
117+
_, err := sink.Step(tk)
109118
return err
110119
case datamodel.Kind_Float:
111120
v, err := n.AsFloat()

codec/dagcbor/roundtrip_test.go

+65
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ package dagcbor
33
import (
44
"bytes"
55
"crypto/rand"
6+
"encoding/hex"
7+
"math"
68
"strings"
79
"testing"
810

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

14+
"github.com/ipld/go-ipld-prime/datamodel"
1215
"github.com/ipld/go-ipld-prime/fluent"
1316
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
1417
"github.com/ipld/go-ipld-prime/node/basicnode"
@@ -115,3 +118,65 @@ func TestRoundtripLinksAndBytes(t *testing.T) {
115118
reconstructed := nb.Build()
116119
qt.Check(t, reconstructed, nodetests.NodeContentEquals, linkByteNode)
117120
}
121+
122+
func TestInts(t *testing.T) {
123+
data := []struct {
124+
name string
125+
hex string
126+
value uint64
127+
intValue int64
128+
intErr string
129+
decodeErr string
130+
}{
131+
{"max uint64", "1bffffffffffffffff", math.MaxUint64, 0, "unsigned integer out of range of int64 type", ""},
132+
{"max int64", "1b7fffffffffffffff", math.MaxInt64, math.MaxInt64, "", ""},
133+
{"1", "01", 1, 1, "", ""},
134+
{"0", "00", 0, 0, "", ""},
135+
{"-1", "20", 0, -1, "", ""},
136+
{"min int64", "3b7fffffffffffffff", 0, math.MinInt64, "", ""},
137+
{"~min uint64", "3bfffffffffffffffe", 0, 0, "", "cbor: negative integer out of rage of int64 type"},
138+
// TODO: 3bffffffffffffffff isn't properly handled by refmt, it's coerced to zero
139+
// MaxUint64 gets overflowed here: https://github.com/polydawn/refmt/blob/30ac6d18308e584ca6a2e74ba81475559db94c5f/cbor/cborDecoderTerminals.go#L75
140+
}
141+
142+
for _, td := range data {
143+
t.Run(td.name, func(t *testing.T) {
144+
buf, err := hex.DecodeString(td.hex) // max uint64
145+
qt.Assert(t, err, qt.IsNil)
146+
nb := basicnode.Prototype.Any.NewBuilder()
147+
err = Decode(nb, bytes.NewReader(buf))
148+
if td.decodeErr != "" {
149+
qt.Assert(t, err, qt.IsNotNil)
150+
qt.Assert(t, err.Error(), qt.Equals, td.decodeErr)
151+
return
152+
}
153+
qt.Assert(t, err, qt.IsNil)
154+
n := nb.Build()
155+
156+
ii, err := n.AsInt()
157+
if td.intErr != "" {
158+
qt.Assert(t, err.Error(), qt.Equals, td.intErr)
159+
} else {
160+
qt.Assert(t, err, qt.IsNil)
161+
qt.Assert(t, ii, qt.Equals, int64(td.intValue))
162+
}
163+
164+
// if the number is outside of the positive int64 range, we should be able
165+
// to access it as a UintNode and be able to access the full int64 range
166+
uin, ok := n.(datamodel.UintNode)
167+
if td.value <= math.MaxInt64 {
168+
qt.Assert(t, ok, qt.IsFalse)
169+
} else {
170+
qt.Assert(t, ok, qt.IsTrue)
171+
val, err := uin.AsUint()
172+
qt.Assert(t, err, qt.IsNil)
173+
qt.Assert(t, val, qt.Equals, uint64(td.value))
174+
}
175+
176+
var byts bytes.Buffer
177+
err = Encode(n, &byts)
178+
qt.Assert(t, err, qt.IsNil)
179+
qt.Assert(t, hex.EncodeToString(byts.Bytes()), qt.Equals, td.hex)
180+
})
181+
}
182+
}

codec/dagcbor/unmarshal.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313

1414
"github.com/ipld/go-ipld-prime/datamodel"
1515
cidlink "github.com/ipld/go-ipld-prime/linking/cid"
16+
"github.com/ipld/go-ipld-prime/node/basicnode"
1617
)
1718

1819
var (
@@ -275,7 +276,12 @@ func unmarshal2(na datamodel.NodeAssembler, tokSrc shared.TokenSource, tk *tok.T
275276
if *gas < 0 {
276277
return ErrAllocationBudgetExceeded
277278
}
278-
return na.AssignInt(int64(tk.Uint)) // FIXME overflow check
279+
// note that this pushes any overflow errors up the stack when AsInt() may
280+
// be called on a UintNode that is too large to cast to an int64
281+
if tk.Uint > math.MaxInt64 {
282+
return na.AssignNode(basicnode.NewUint(tk.Uint))
283+
}
284+
return na.AssignInt(int64(tk.Uint))
279285
case tok.TFloat64:
280286
*gas -= 1
281287
if *gas < 0 {

datamodel/node.go

+15
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,21 @@ type Node interface {
167167
Prototype() NodePrototype
168168
}
169169

170+
// UintNode is an optional interface that can be used to represent an Int node
171+
// that provides access to the full uint64 range.
172+
//
173+
// EXPERIMENTAL: this API is experimental and may be changed or removed in a
174+
// future use. A future iteration may replace this with a BigInt interface to
175+
// access a larger range of integers that may be enabled by alternative codecs.
176+
type UintNode interface {
177+
Node
178+
179+
// AsUint returns a uint64 representing the underlying integer if possible.
180+
// This may return an error if the Node represents a negative integer that
181+
// cannot be represented as a uint64.
182+
AsUint() (uint64, error)
183+
}
184+
170185
// LargeBytesNode is an optional interface extending a Bytes node that allows its
171186
// contents to be accessed through an io.ReadSeeker instead of a []byte slice. Use of
172187
// an io.Reader is encouraged, as it allows for streaming large byte slices

node/basicnode/int.go

+85-3
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,40 @@
11
package basicnode
22

33
import (
4+
"fmt"
5+
"math"
6+
47
"github.com/ipld/go-ipld-prime/datamodel"
58
"github.com/ipld/go-ipld-prime/node/mixins"
69
)
710

811
var (
912
_ datamodel.Node = plainInt(0)
13+
_ datamodel.Node = plainUint(0)
14+
_ datamodel.UintNode = plainUint(0)
1015
_ datamodel.NodePrototype = Prototype__Int{}
1116
_ datamodel.NodeBuilder = &plainInt__Builder{}
1217
_ datamodel.NodeAssembler = &plainInt__Assembler{}
1318
)
1419

1520
func NewInt(value int64) datamodel.Node {
16-
v := plainInt(value)
17-
return &v
21+
return plainInt(value)
22+
}
23+
24+
// NewUint creates a new uint64-backed Node which will behave as a plain Int
25+
// node but also conforms to the datamodel.UintNode interface which can access
26+
// the full uint64 range.
27+
//
28+
// EXPERIMENTAL: this API is experimental and may be changed or removed in a
29+
// future release.
30+
func NewUint(value uint64) datamodel.Node {
31+
return plainUint(value)
1832
}
1933

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

23-
// -- Node interface methods -->
37+
// -- Node interface methods for plainInt -->
2438

2539
func (plainInt) Kind() datamodel.Kind {
2640
return datamodel.Kind_Int
@@ -74,6 +88,74 @@ func (plainInt) Prototype() datamodel.NodePrototype {
7488
return Prototype__Int{}
7589
}
7690

91+
// plainUint is a simple boxed uint64 that complies with datamodel.Node,
92+
// allowing representation of the uint64 range above the int64 maximum via the
93+
// UintNode interface
94+
type plainUint uint64
95+
96+
// -- Node interface methods for plainUint -->
97+
98+
func (plainUint) Kind() datamodel.Kind {
99+
return datamodel.Kind_Int
100+
}
101+
func (plainUint) LookupByString(string) (datamodel.Node, error) {
102+
return mixins.Int{TypeName: "int"}.LookupByString("")
103+
}
104+
func (plainUint) LookupByNode(key datamodel.Node) (datamodel.Node, error) {
105+
return mixins.Int{TypeName: "int"}.LookupByNode(nil)
106+
}
107+
func (plainUint) LookupByIndex(idx int64) (datamodel.Node, error) {
108+
return mixins.Int{TypeName: "int"}.LookupByIndex(0)
109+
}
110+
func (plainUint) LookupBySegment(seg datamodel.PathSegment) (datamodel.Node, error) {
111+
return mixins.Int{TypeName: "int"}.LookupBySegment(seg)
112+
}
113+
func (plainUint) MapIterator() datamodel.MapIterator {
114+
return nil
115+
}
116+
func (plainUint) ListIterator() datamodel.ListIterator {
117+
return nil
118+
}
119+
func (plainUint) Length() int64 {
120+
return -1
121+
}
122+
func (plainUint) IsAbsent() bool {
123+
return false
124+
}
125+
func (plainUint) IsNull() bool {
126+
return false
127+
}
128+
func (plainUint) AsBool() (bool, error) {
129+
return mixins.Int{TypeName: "int"}.AsBool()
130+
}
131+
func (n plainUint) AsInt() (int64, error) {
132+
if uint64(n) > uint64(math.MaxInt64) {
133+
return -1, fmt.Errorf("unsigned integer out of range of int64 type")
134+
}
135+
return int64(n), nil
136+
}
137+
func (plainUint) AsFloat() (float64, error) {
138+
return mixins.Int{TypeName: "int"}.AsFloat()
139+
}
140+
func (plainUint) AsString() (string, error) {
141+
return mixins.Int{TypeName: "int"}.AsString()
142+
}
143+
func (plainUint) AsBytes() ([]byte, error) {
144+
return mixins.Int{TypeName: "int"}.AsBytes()
145+
}
146+
func (plainUint) AsLink() (datamodel.Link, error) {
147+
return mixins.Int{TypeName: "int"}.AsLink()
148+
}
149+
func (plainUint) Prototype() datamodel.NodePrototype {
150+
return Prototype__Int{}
151+
}
152+
153+
// allows plainUint to conform to the plainUint interface
154+
155+
func (n plainUint) AsUint() (uint64, error) {
156+
return uint64(n), nil
157+
}
158+
77159
// -- NodePrototype -->
78160

79161
type Prototype__Int struct{}

0 commit comments

Comments
 (0)