-
Notifications
You must be signed in to change notification settings - Fork 3.7k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: deterministic CBOR encoding of textual rendering (#13697)
* feat: deterministic CBOR encoding of textual rendering * refactor: cbor package to internal, test cases as json * chore: silence spurious gosec warnings * docs: review feedback
- Loading branch information
Showing
6 changed files
with
491 additions
and
1 deletion.
There are no files selected for viewing
This file contains 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
This file contains 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,238 @@ | ||
// Package cbor implements just enough of the CBOR (Concise Binary Object | ||
// Representation, RFC 8948) to deterministically encode simple data. | ||
package cbor | ||
|
||
import ( | ||
"bytes" | ||
"encoding/binary" | ||
"fmt" | ||
"io" | ||
"math" | ||
"sort" | ||
) | ||
|
||
const ( | ||
major_uint byte = 0 | ||
major_negint byte = 1 | ||
major_byte_string byte = 2 | ||
major_text_string byte = 3 | ||
major_array byte = 4 | ||
major_map byte = 5 | ||
major_tagged byte = 6 | ||
major_simple byte = 7 | ||
) | ||
|
||
func encode_first_byte(major byte, extra byte) byte { | ||
return (major << 5) | extra&0x1F | ||
} | ||
|
||
func encode_prefix(major byte, arg uint64, w io.Writer) error { | ||
switch { | ||
case arg < 24: | ||
_, err := w.Write([]byte{encode_first_byte(major, byte(arg))}) | ||
return err | ||
case arg <= math.MaxUint8: | ||
_, err := w.Write([]byte{encode_first_byte(major, 24), byte(arg)}) | ||
return err | ||
case arg <= math.MaxUint16: | ||
_, err := w.Write([]byte{encode_first_byte(major, 25)}) | ||
if err != nil { | ||
return err | ||
} | ||
// #nosec G701 | ||
// Since we're under the limit, narrowing is safe. | ||
return binary.Write(w, binary.BigEndian, uint16(arg)) | ||
case arg <= math.MaxUint32: | ||
_, err := w.Write([]byte{encode_first_byte(major, 26)}) | ||
if err != nil { | ||
return err | ||
} | ||
// #nosec G701 | ||
// Since we're under the limit, narrowing is safe. | ||
return binary.Write(w, binary.BigEndian, uint32(arg)) | ||
} | ||
_, err := w.Write([]byte{encode_first_byte(major, 27)}) | ||
if err != nil { | ||
return err | ||
} | ||
return binary.Write(w, binary.BigEndian, arg) | ||
} | ||
|
||
// Cbor is a CBOR (RFC8949) data item that can be encoded to a stream. | ||
type Cbor interface { | ||
// Encode deterministically writes the CBOR-encoded data to the stream. | ||
Encode(w io.Writer) error | ||
} | ||
|
||
// Uint is the CBOR unsigned integer type. | ||
type Uint uint64 | ||
|
||
// NewUint returns a CBOR unsigned integer data item. | ||
func NewUint(n uint64) Uint { | ||
return Uint(n) | ||
} | ||
|
||
var _ Cbor = NewUint(0) | ||
|
||
// Encode implements the Cbor interface. | ||
func (n Uint) Encode(w io.Writer) error { | ||
// #nosec G701 | ||
// Widening is safe. | ||
return encode_prefix(major_uint, uint64(n), w) | ||
} | ||
|
||
// Text is the CBOR text string type. | ||
type Text string | ||
|
||
// NewText returns a CBOR text string data item. | ||
func NewText(s string) Text { | ||
return Text(s) | ||
} | ||
|
||
var _ Cbor = NewText("") | ||
|
||
// Encode implements the Cbor interface. | ||
func (s Text) Encode(w io.Writer) error { | ||
err := encode_prefix(major_text_string, uint64(len(s)), w) | ||
if err != nil { | ||
return err | ||
} | ||
_, err = w.Write([]byte(string(s))) | ||
return err | ||
} | ||
|
||
// Array is the CBOR array type. | ||
type Array struct { | ||
elts []Cbor | ||
} | ||
|
||
// NewArray reutnrs a CBOR array data item, | ||
// containing the specified elements. | ||
func NewArray(elts ...Cbor) Array { | ||
return Array{elts: elts} | ||
} | ||
|
||
var _ Cbor = NewArray() | ||
|
||
// Append appends CBOR data items to an existing Array. | ||
func (a Array) Append(c Cbor) Array { | ||
a.elts = append(a.elts, c) | ||
return a | ||
} | ||
|
||
// Encode implements the Cbor interface. | ||
func (a Array) Encode(w io.Writer) error { | ||
err := encode_prefix(major_array, uint64(len(a.elts)), w) | ||
if err != nil { | ||
return err | ||
} | ||
for _, elt := range a.elts { | ||
err = elt.Encode(w) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// Entry is a key/value pair in a CBOR map. | ||
type Entry struct { | ||
key, val Cbor | ||
} | ||
|
||
// NewEntry returns a CBOR key/value pair for use in a Map. | ||
func NewEntry(key, val Cbor) Entry { | ||
return Entry{key: key, val: val} | ||
} | ||
|
||
// Map is the CBOR map type. | ||
type Map struct { | ||
entries []Entry | ||
} | ||
|
||
// NewMap returns a CBOR map data item containing the specified entries. | ||
// Duplicate keys in the Map will cause an error when Encode is called. | ||
func NewMap(entries ...Entry) Map { | ||
return Map{entries: entries} | ||
} | ||
|
||
// Add adds a key/value entry to an existimg Map. | ||
// Duplicate keys in the Map will cause an error when Encode is called. | ||
func (m Map) Add(key, val Cbor) Map { | ||
m.entries = append(m.entries, NewEntry(key, val)) | ||
return m | ||
} | ||
|
||
type keyIdx struct { | ||
key []byte | ||
idx int | ||
} | ||
|
||
// Encode implements the Cbor interface. | ||
func (m Map) Encode(w io.Writer) error { | ||
err := encode_prefix(major_map, uint64(len(m.entries)), w) | ||
if err != nil { | ||
return err | ||
} | ||
// For deterministic encoding, map entries must be sorted by their | ||
// encoded keys in bytewise lexicographic order (RFC 8949, section 4.2.1). | ||
renderedKeys := make([]keyIdx, len(m.entries)) | ||
for i, entry := range m.entries { | ||
var buf bytes.Buffer | ||
err := entry.key.Encode(&buf) | ||
if err != nil { | ||
return err | ||
} | ||
renderedKeys[i] = keyIdx{key: buf.Bytes(), idx: i} | ||
} | ||
sort.SliceStable(renderedKeys, func(i, j int) bool { | ||
return bytes.Compare(renderedKeys[i].key, renderedKeys[j].key) < 0 | ||
}) | ||
var prevKey []byte | ||
for i, rk := range renderedKeys { | ||
if i > 0 && bytes.Equal(prevKey, rk.key) { | ||
return fmt.Errorf("duplicate map keys at %d and %d", rk.idx, renderedKeys[i-1].idx) | ||
} | ||
prevKey = rk.key | ||
_, err = w.Write(rk.key) | ||
if err != nil { | ||
return err | ||
} | ||
err = m.entries[rk.idx].val.Encode(w) | ||
if err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
const ( | ||
simple_false byte = 20 | ||
simple_true byte = 21 | ||
simple_null byte = 22 | ||
simple_undefined byte = 32 | ||
) | ||
|
||
func encodeSimple(b byte, w io.Writer) error { | ||
// #nosec G701 | ||
// Widening is safe. | ||
return encode_prefix(major_simple, uint64(b), w) | ||
} | ||
|
||
// Bool is the type of CBOR booleans. | ||
type Bool byte | ||
|
||
// NewBool returns a CBOR boolean data item. | ||
func NewBool(b bool) Bool { | ||
if b { | ||
return Bool(simple_true) | ||
} | ||
return Bool(simple_false) | ||
} | ||
|
||
var _ Cbor = NewBool(false) | ||
|
||
// Encode implements the Cbor interface. | ||
func (b Bool) Encode(w io.Writer) error { | ||
return encodeSimple(byte(b), w) | ||
} |
This file contains 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,103 @@ | ||
package cbor_test | ||
|
||
import ( | ||
"bytes" | ||
"encoding/hex" | ||
"fmt" | ||
"testing" | ||
|
||
"cosmossdk.io/tx/textual/internal/cbor" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
var ( | ||
ui = cbor.NewUint | ||
txt = cbor.NewText | ||
arr = cbor.NewArray | ||
mp = cbor.NewMap | ||
ent = cbor.NewEntry | ||
) | ||
|
||
func TestCborRFC(t *testing.T) { | ||
for i, tc := range []struct { | ||
cb cbor.Cbor | ||
encoding string | ||
expectError bool | ||
}{ | ||
// Examples come from RFC8949, Appendix A | ||
{cb: ui(0), encoding: "00"}, | ||
{cb: ui(1), encoding: "01"}, | ||
{cb: ui(10), encoding: "0a"}, | ||
{cb: ui(23), encoding: "17"}, | ||
{cb: ui(24), encoding: "1818"}, | ||
{cb: ui(25), encoding: "1819"}, | ||
{cb: ui(100), encoding: "1864"}, | ||
{cb: ui(1000), encoding: "1903e8"}, | ||
{cb: ui(1000000), encoding: "1a000f4240"}, | ||
{cb: ui(1000000000000), encoding: "1b000000e8d4a51000"}, | ||
{cb: ui(18446744073709551615), encoding: "1bffffffffffffffff"}, | ||
{cb: cbor.NewBool(false), encoding: "f4"}, | ||
{cb: cbor.NewBool(true), encoding: "f5"}, | ||
{cb: txt(""), encoding: "60"}, | ||
{cb: txt("a"), encoding: "6161"}, | ||
{cb: txt("IETF"), encoding: "6449455446"}, | ||
{cb: txt("\"\\"), encoding: "62225c"}, | ||
{cb: txt("\u00fc"), encoding: "62c3bc"}, | ||
{cb: txt("\u6c34"), encoding: "63e6b0b4"}, | ||
// Go doesn't like string literals with surrogate pairs, create manually | ||
{cb: txt(string([]byte{0xf0, 0x90, 0x85, 0x91})), encoding: "64f0908591"}, | ||
{cb: arr(), encoding: "80"}, | ||
{cb: arr(ui(1), ui(2)).Append(ui(3)), encoding: "83010203"}, | ||
{ | ||
cb: arr(ui(1)). | ||
Append(arr(ui(2), ui(3))). | ||
Append(arr().Append(ui(4)).Append(ui(5))), | ||
encoding: "8301820203820405", | ||
}, | ||
{ | ||
cb: arr( | ||
ui(1), ui(2), ui(3), ui(4), ui(5), | ||
ui(6), ui(7), ui(8), ui(9), ui(10), | ||
ui(11), ui(12), ui(13), ui(14), ui(15), | ||
ui(16), ui(17), ui(18), ui(19), ui(20), | ||
ui(21), ui(22), ui(23), ui(24), ui(25)), | ||
encoding: "98190102030405060708090a0b0c0d0e0f101112131415161718181819", | ||
}, | ||
{cb: mp(), encoding: "a0"}, | ||
{cb: mp(ent(ui(1), ui(2))).Add(ui(3), ui(4)), encoding: "a201020304"}, | ||
{cb: mp(ent(txt("a"), ui(1)), ent(txt("b"), arr(ui(2), ui(3)))), encoding: "a26161016162820203"}, | ||
{cb: arr(txt("a"), mp(ent(txt("b"), txt("c")))), encoding: "826161a161626163"}, | ||
{ | ||
cb: mp( | ||
ent(txt("a"), txt("A")), | ||
ent(txt("b"), txt("B")), | ||
ent(txt("c"), txt("C")), | ||
ent(txt("d"), txt("D")), | ||
ent(txt("e"), txt("E"))), | ||
encoding: "a56161614161626142616361436164614461656145", | ||
}, | ||
// Departing from the RFC | ||
{cb: mp(ent(ui(1), ui(2)), ent(ui(1), ui(2))), expectError: true}, | ||
// Map has deterministic order based on key encoding | ||
{ | ||
cb: mp( | ||
ent(txt("aa"), ui(0)), | ||
ent(txt("a"), ui(2)), | ||
ent(ui(1), txt("b"))), | ||
encoding: "a301616261610262616100", | ||
}, | ||
} { | ||
t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { | ||
var buf bytes.Buffer | ||
err := tc.cb.Encode(&buf) | ||
if tc.expectError { | ||
require.Error(t, err) | ||
return | ||
} | ||
require.NoError(t, err) | ||
want, err := hex.DecodeString(tc.encoding) | ||
require.NoError(t, err) | ||
require.Equal(t, want, buf.Bytes()) | ||
}) | ||
} | ||
} |
Oops, something went wrong.