-
Notifications
You must be signed in to change notification settings - Fork 239
feat(cborlite): add the CBOR decoder primitives #3934
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
Draft
Draft
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| package cborlite_test | ||
|
|
||
| import ( | ||
| "math" | ||
| "testing" | ||
|
|
||
| "github.com/NethermindEth/juno/encoder/cborlite" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| type reader struct { | ||
| name string | ||
| read func(data []byte) (int, bool) | ||
| } | ||
|
|
||
| func readers() []reader { | ||
| return []reader{ | ||
| {"Head", func(data []byte) (int, bool) { | ||
| _, _, consumed, ok := cborlite.Head(data) | ||
| return consumed, ok | ||
| }}, | ||
| {"Skip", cborlite.Skip}, | ||
| {"BytesNoCopy", func(data []byte) (int, bool) { | ||
| _, consumed, ok := cborlite.BytesNoCopy(data) | ||
| return consumed, ok | ||
| }}, | ||
| {"Bytes", func(data []byte) (int, bool) { | ||
| _, consumed, ok := cborlite.Bytes(data) | ||
| return consumed, ok | ||
| }}, | ||
| {"StringNoCopy", func(data []byte) (int, bool) { | ||
| _, consumed, ok := cborlite.StringNoCopy(data) | ||
| return consumed, ok | ||
| }}, | ||
| {"String", func(data []byte) (int, bool) { | ||
| _, consumed, ok := cborlite.String(data) | ||
| return consumed, ok | ||
| }}, | ||
| {"Uint64", func(data []byte) (int, bool) { | ||
| _, consumed, ok := cborlite.Uint64(data) | ||
| return consumed, ok | ||
| }}, | ||
| {"Int64", func(data []byte) (int, bool) { | ||
| _, consumed, ok := cborlite.Int64(data) | ||
| return consumed, ok | ||
| }}, | ||
| {"BigInt", func(data []byte) (int, bool) { | ||
| _, consumed, ok := cborlite.BigInt(data) | ||
| return consumed, ok | ||
| }}, | ||
| {"Bool", func(data []byte) (int, bool) { | ||
| _, consumed, ok := cborlite.Bool(data) | ||
| return consumed, ok | ||
| }}, | ||
| {"Tag", func(data []byte) (int, bool) { | ||
| _, consumed, ok := cborlite.Tag(data) | ||
| return consumed, ok | ||
| }}, | ||
| {"ArrayHeader", func(data []byte) (int, bool) { | ||
| _, consumed, ok := cborlite.ArrayHeader(data) | ||
| return consumed, ok | ||
| }}, | ||
| {"MapHeader", func(data []byte) (int, bool) { | ||
| _, consumed, ok := cborlite.MapHeader(data) | ||
| return consumed, ok | ||
| }}, | ||
| {"ReadNull", cborlite.ReadNull}, | ||
| } | ||
| } | ||
|
|
||
| func FuzzReadersStayInBounds(f *testing.F) { | ||
| f.Add([]byte{}) | ||
| f.Add(head(uintMajor, 0)) | ||
| f.Add([]byte{null}) | ||
| f.Add([]byte{simpleFalse}) | ||
| f.Add([]byte{simpleTrue}) | ||
| f.Add(head(simpleMajor, 23)) // undefined, which is neither null nor a boolean | ||
| f.Add(head(simpleMajor, 32)) | ||
| f.Add(head(uintMajor, 255)) | ||
| f.Add(head(uintMajor, math.MaxUint64)) | ||
| f.Add(cborBytes(0xaa, 0xbb, 0xcc)) | ||
| f.Add(cborText("abc")) | ||
| f.Add(cborArray(head(uintMajor, 1), head(uintMajor, 2))) | ||
| f.Add(cborMap(cborText("k"), head(uintMajor, 1))) | ||
| f.Add(cborTagged(tagPositiveBignum, cborBytes(beyondUint64.Bytes()...))) | ||
| f.Add(cborArray( | ||
| cborArray(head(uintMajor, 1), head(uintMajor, 2)), | ||
| cborMap(head(uintMajor, 3), head(uintMajor, 4)), | ||
| )) | ||
| f.Add([]byte{initialByte(arrayMajor, 31), 1, 0xff}) // Indefinite length. | ||
| f.Add([]byte{initialByte(arrayMajor, info1Byte), 255, 0x01}) // Too little bytes. | ||
|
|
||
| all := readers() | ||
|
|
||
| f.Fuzz(func(t *testing.T, data []byte) { | ||
| if len(data) > 1024 { | ||
| return | ||
| } | ||
|
|
||
| points := []int{0, 1, len(data) / 2, len(data)} | ||
|
|
||
| for _, r := range all { | ||
| for _, at := range points { | ||
| if at > len(data) { | ||
| continue | ||
| } | ||
|
|
||
| window := data[at:] | ||
| consumed, ok := r.read(window) | ||
| if !ok { | ||
| continue | ||
| } | ||
|
|
||
| require.Positivef(t, consumed, | ||
| "%s reported success without making progress on %d bytes", | ||
| r.name, len(window)) | ||
| require.LessOrEqualf(t, consumed, len(window), | ||
| "%s reported consuming %d of %d bytes", | ||
| r.name, consumed, len(window)) | ||
| } | ||
| } | ||
| }) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,98 @@ | ||
| package cborlite | ||
|
|
||
| import "encoding/binary" | ||
|
|
||
| const ( | ||
| // MajorMask and InfoMask split a header byte into | ||
| // major type (top 3 bits) and additional info (low 5 bits). | ||
| MajorMask = 0b1110_0000 | ||
| InfoMask = 0b0001_1111 | ||
|
|
||
| // Major types. They define the object type. | ||
| // See https://www.rfc-editor.org/rfc/rfc8949.html#section-3.1 | ||
| UintMajor = 0 << 5 | ||
| NegIntMajor = 1 << 5 | ||
| BytesMajor = 2 << 5 | ||
| StringMajor = 3 << 5 | ||
| ArrayMajor = 4 << 5 | ||
| MapMajor = 5 << 5 | ||
| TagMajor = 6 << 5 | ||
| SimpleMajor = 7 << 5 | ||
|
|
||
| // bool values and nil pointers. | ||
| SimpleFalse = SimpleMajor | 20 | ||
| SimpleTrue = SimpleMajor | 21 | ||
| Null = SimpleMajor | 22 | ||
|
|
||
| // Tag types to define Big Numbers | ||
| // https://www.rfc-editor.org/rfc/rfc8949.html#section-3.4.3 | ||
| TagPositiveBignum = 2 | ||
| TagNegativeBignum = 3 | ||
|
|
||
| // Additional info values that say how many bytes follow the argument. | ||
| Info1Byte = 24 | ||
| Info2Byte = 25 | ||
| Info4Byte = 26 | ||
| Info8Byte = 27 | ||
|
|
||
| // Always one byte. | ||
| headerSize = 1 | ||
| ) | ||
|
|
||
| // Head reads the header at the start of data. | ||
| // argument is a value for a scalar, the length for a string, or an item count for an array/map. | ||
| func Head(data []byte) (major byte, argument uint64, consumed int, ok bool) { | ||
| if len(data) == 0 { | ||
| return 0, 0, 0, false | ||
| } | ||
|
|
||
| header := data[0] | ||
| major = header & MajorMask | ||
| info := header & InfoMask | ||
|
|
||
| // The argument is so small it fits inside info. | ||
| if info < Info1Byte { | ||
| return major, uint64(info), headerSize, true | ||
| } | ||
|
|
||
| // info says the number of bytes that follow, max 8. | ||
| if info > Info8Byte { | ||
| return 0, 0, 0, false | ||
| } | ||
| infoByteSize := 1 << (info - Info1Byte) | ||
| if len(data) < headerSize+infoByteSize { | ||
| return 0, 0, 0, false | ||
| } | ||
|
|
||
| switch infoByteSize { | ||
| case 1: | ||
| argument = uint64(data[headerSize]) | ||
| case 2: | ||
| argument = uint64(binary.BigEndian.Uint16(data[headerSize:])) | ||
| case 4: | ||
| argument = uint64(binary.BigEndian.Uint32(data[headerSize:])) | ||
| default: | ||
| argument = binary.BigEndian.Uint64(data[headerSize:]) | ||
| } | ||
| return major, argument, headerSize + infoByteSize, true | ||
| } | ||
|
|
||
| // ArrayHeader reads an array header and its element count. | ||
| func ArrayHeader(data []byte) (length, consumed int, ok bool) { | ||
| major, count, consumed, ok := Head(data) | ||
| // An element takes at least one byte, data must have space for it. | ||
| if !ok || major != ArrayMajor || count > uint64(len(data)-consumed) { | ||
| return 0, 0, false | ||
| } | ||
| return int(count), consumed, true | ||
| } | ||
|
RafaelGranza marked this conversation as resolved.
|
||
|
|
||
| // MapHeader reads a map header and its pair count. | ||
| func MapHeader(data []byte) (pairsCount, consumed int, ok bool) { | ||
| major, count, consumed, ok := Head(data) | ||
| // A pair takes at least two bytes, data must have space for it. | ||
| if !ok || major != MapMajor || count > uint64(len(data)-consumed)/2 { | ||
| return 0, 0, false | ||
| } | ||
| return int(count), consumed, true | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,167 @@ | ||
| package cborlite_test | ||
|
|
||
| import ( | ||
| "math" | ||
| "testing" | ||
|
|
||
| "github.com/NethermindEth/juno/encoder/cborlite" | ||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestHead(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| data []byte | ||
| major byte | ||
| argument uint64 | ||
| consumed int | ||
| ok bool | ||
| }{ | ||
| { | ||
| name: "argument inside the header byte", | ||
| data: head(uintMajor, 23), | ||
| major: uintMajor, argument: 23, consumed: 1, ok: true, | ||
| }, | ||
| { | ||
| name: "one byte argument", | ||
| data: head(uintMajor, 255), | ||
| major: uintMajor, argument: 255, consumed: 2, ok: true, | ||
| }, | ||
| { | ||
| name: "two byte argument", | ||
| data: head(uintMajor, 256), | ||
| major: uintMajor, argument: 256, consumed: 3, ok: true, | ||
| }, | ||
| { | ||
| name: "four byte argument", | ||
| data: head(uintMajor, 65536), | ||
| major: uintMajor, argument: 65536, consumed: 5, ok: true, | ||
| }, | ||
| { | ||
| name: "eight byte argument", | ||
| data: head(uintMajor, 1<<32), | ||
| major: uintMajor, argument: 1 << 32, consumed: 9, ok: true, | ||
| }, | ||
| { | ||
| // The reader stops at the end of the header and never looks at what follows | ||
| name: "reads only the header, whatever trails it", | ||
| data: cborText("abc"), | ||
| major: stringMajor, argument: 3, consumed: 1, ok: true, | ||
| }, | ||
| { | ||
| name: "major type is decoded independently of the argument", | ||
| data: []byte{initialByte(mapMajor, 1)}, major: mapMajor, argument: 1, consumed: 1, ok: true, | ||
| }, | ||
| {name: "empty buffer", data: []byte{}}, | ||
| {name: "one byte argument truncated", data: []byte{initialByte(uintMajor, info1Byte)}}, | ||
| {name: "two byte argument truncated", data: []byte{initialByte(uintMajor, info2Byte), 0x01}}, | ||
| { | ||
| name: "four byte argument truncated", | ||
| data: []byte{initialByte(uintMajor, info4Byte), 0x00, 0x01}, | ||
| }, | ||
| { | ||
| name: "eight byte argument truncated", | ||
| data: []byte{initialByte(uintMajor, info8Byte), 0, 0, 0, 1}, | ||
| }, | ||
| {name: "reserved additional info 28", data: headerFollowedBy(initialByte(uintMajor, 28), 16)}, | ||
| {name: "reserved additional info 29", data: headerFollowedBy(initialByte(uintMajor, 29), 32)}, | ||
| {name: "reserved additional info 30", data: headerFollowedBy(initialByte(uintMajor, 30), 64)}, | ||
| {name: "indefinite length", data: headerFollowedBy(initialByte(uintMajor, 31), 128)}, | ||
| } | ||
|
|
||
| for _, test := range tests { | ||
| t.Run(test.name, func(t *testing.T) { | ||
| major, argument, consumed, ok := cborlite.Head(test.data) | ||
|
|
||
| require.Equal(t, test.ok, ok) | ||
| if !test.ok { | ||
| return | ||
| } | ||
| assert.Equal(t, test.major, major) | ||
| assert.Equal(t, test.argument, argument) | ||
| assert.Equal(t, test.consumed, consumed) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestArrayHeader(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| data []byte | ||
| length int | ||
| consumed int | ||
| ok bool | ||
| }{ | ||
| { | ||
| name: "count and header width", | ||
| data: cborArray(head(uintMajor, 1), head(uintMajor, 2)), | ||
| length: 2, consumed: 1, ok: true, | ||
| }, | ||
| {name: "empty array is zero", data: cborArray(), consumed: 1, ok: true}, | ||
| // Null is a different item, not an array of no elements. | ||
| {name: "declines null", data: []byte{null}}, | ||
| // Guards the allocation: every element needs at least one byte. | ||
| { | ||
| name: "rejects a count past the remaining bytes", | ||
| data: []byte{initialByte(arrayMajor, 5), 0x01, 0x02}, | ||
| }, | ||
| // The count is returned as an int, so this one is the case that would come back | ||
| // negative and make every length check downstream read backwards. | ||
| {name: "rejects a count of all ones", data: head(arrayMajor, math.MaxUint64)}, | ||
| { | ||
| name: "rejects another major type", | ||
| data: cborMap(head(uintMajor, 1), head(uintMajor, 2)), | ||
| }, | ||
| } | ||
|
|
||
| for _, test := range tests { | ||
| t.Run(test.name, func(t *testing.T) { | ||
| length, consumed, ok := cborlite.ArrayHeader(test.data) | ||
|
|
||
| require.Equal(t, test.ok, ok) | ||
| if !test.ok { | ||
| return | ||
| } | ||
| assert.Equal(t, test.length, length) | ||
| assert.Equal(t, test.consumed, consumed) | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| func TestMapHeader(t *testing.T) { | ||
| tests := []struct { | ||
| name string | ||
| data []byte | ||
| pairs int | ||
| consumed int | ||
| ok bool | ||
| }{ | ||
| { | ||
| name: "pair count and header width", | ||
| data: cborMap(head(uintMajor, 1), head(uintMajor, 2)), | ||
| pairs: 1, consumed: 1, ok: true, | ||
| }, | ||
| {name: "empty map is zero", data: cborMap(), consumed: 1, ok: true}, | ||
| {name: "declines null", data: []byte{null}}, | ||
| // A pair takes two bytes at the very least, so this count cannot be honoured. | ||
| {name: "rejects a count past the remaining bytes", data: []byte{initialByte(mapMajor, 5), 0x01}}, | ||
| { | ||
| name: "rejects an array", | ||
| data: cborArray(head(uintMajor, 1), head(uintMajor, 2)), | ||
| }, | ||
| } | ||
|
|
||
| for _, test := range tests { | ||
| t.Run(test.name, func(t *testing.T) { | ||
| pairs, consumed, ok := cborlite.MapHeader(test.data) | ||
|
|
||
| require.Equal(t, test.ok, ok) | ||
| if !test.ok { | ||
| return | ||
| } | ||
| assert.Equal(t, test.pairs, pairs) | ||
| assert.Equal(t, test.consumed, consumed) | ||
| }) | ||
| } | ||
| } |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.