From 1a2ecefdc5fc5fc5211062b860cc92d5e2cc47d3 Mon Sep 17 00:00:00 2001 From: Rod Vagg Date: Fri, 13 May 2022 11:13:27 +1000 Subject: [PATCH] feat: introduce UIntNode interface, use it exclusively for >int64 --- codec/dagcbor/marshal.go | 26 ++++---- codec/dagcbor/roundtrip_test.go | 73 +++++++++++++++++++++ codec/dagcbor/unmarshal.go | 5 +- codec/dagcbor/unmarshal_test.go | 27 -------- datamodel/node.go | 11 ++++ node/basicnode/int.go | 112 ++++++++++++++++++++++++-------- node/basicnode/list.go | 2 +- node/basicnode/map.go | 2 +- 8 files changed, 187 insertions(+), 71 deletions(-) diff --git a/codec/dagcbor/marshal.go b/codec/dagcbor/marshal.go index 3e70876d..b3ffa45a 100644 --- a/codec/dagcbor/marshal.go +++ b/codec/dagcbor/marshal.go @@ -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 { @@ -105,10 +100,13 @@ 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 { @@ -116,14 +114,14 @@ func marshal(n datamodel.Node, tk *tok.Token, sink shared.TokenSink, options Enc } 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 diff --git a/codec/dagcbor/roundtrip_test.go b/codec/dagcbor/roundtrip_test.go index babce5e5..bb41499a 100644 --- a/codec/dagcbor/roundtrip_test.go +++ b/codec/dagcbor/roundtrip_test.go @@ -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" @@ -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) + }) +} diff --git a/codec/dagcbor/unmarshal.go b/codec/dagcbor/unmarshal.go index 5a8c34ff..56658367 100644 --- a/codec/dagcbor/unmarshal.go +++ b/codec/dagcbor/unmarshal.go @@ -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 { diff --git a/codec/dagcbor/unmarshal_test.go b/codec/dagcbor/unmarshal_test.go index 801f8325..658711ee 100644 --- a/codec/dagcbor/unmarshal_test.go +++ b/codec/dagcbor/unmarshal_test.go @@ -1,8 +1,6 @@ package dagcbor import ( - "bytes" - "encoding/hex" "runtime" "strings" "testing" @@ -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") -} diff --git a/datamodel/node.go b/datamodel/node.go index 30ae6bc8..4d6b430f 100644 --- a/datamodel/node.go +++ b/datamodel/node.go @@ -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 diff --git a/node/basicnode/int.go b/node/basicnode/int.go index 38504634..a180484c 100644 --- a/node/basicnode/int.go +++ b/node/basicnode/int.go @@ -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 @@ -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() @@ -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 --> @@ -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 { @@ -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 } } diff --git a/node/basicnode/list.go b/node/basicnode/list.go index d14a7fb9..6f7582bb 100644 --- a/node/basicnode/list.go +++ b/node/basicnode/list.go @@ -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 { diff --git a/node/basicnode/map.go b/node/basicnode/map.go index b1b4b674..9a86fc52 100644 --- a/node/basicnode/map.go +++ b/node/basicnode/map.go @@ -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 {