Skip to content

Commit ec7b4c2

Browse files
authored
refactor: preserve map key order on encode/decode (#123)
Fixes #122 Signed-off-by: Aurora Gaffney <[email protected]>
1 parent a1854ed commit ec7b4c2

File tree

3 files changed

+78
-16
lines changed

3 files changed

+78
-16
lines changed

data/data_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,26 @@ var testDefs = []struct {
177177
),
178178
CborHex: "d87a81d87980",
179179
},
180+
{
181+
Data: NewMap(
182+
[][2]PlutusData{
183+
{
184+
NewInteger(big.NewInt(2)),
185+
NewInteger(big.NewInt(2)),
186+
},
187+
{
188+
NewInteger(big.NewInt(3)),
189+
NewInteger(big.NewInt(3)),
190+
},
191+
{
192+
NewInteger(big.NewInt(1)),
193+
NewInteger(big.NewInt(1)),
194+
},
195+
},
196+
),
197+
// {2:2,3:3,1:1}
198+
CborHex: "a3020203030101",
199+
},
180200
}
181201

182202
func TestPlutusDataEncode(t *testing.T) {

data/decode.go

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package data
22

33
import (
4-
"bytes"
54
"fmt"
65
"math/big"
76

@@ -33,8 +32,6 @@ func Decode(b []byte) (PlutusData, error) {
3332

3433
// cborUnmarshal acts like cbor.Unmarshal but allows us to set our own decoder options
3534
func cborUnmarshal(dataBytes []byte, dest any) error {
36-
data := bytes.NewReader(dataBytes)
37-
// Create a custom decoder that returns an error on unknown fields
3835
decOptions := cbor.DecOptions{
3936
// This defaults to 32, but there are blocks in the wild using >64 nested levels
4037
MaxNestedLevels: 256,
@@ -43,8 +40,7 @@ func cborUnmarshal(dataBytes []byte, dest any) error {
4340
if err != nil {
4441
return err
4542
}
46-
dec := decMode.NewDecoder(data)
47-
return dec.Decode(dest)
43+
return decMode.Unmarshal(dataBytes, dest)
4844
}
4945

5046
// decodeCborRaw is an alternative to cbor.Unmarshal() that converts cbor.Tag to Constr
@@ -103,21 +99,46 @@ func decodeCborRawList(data []byte) (any, error) {
10399
}
104100

105101
func decodeCborRawMap(data []byte) (any, error) {
102+
// The below is a hack to work around our CBOR library not supporting preserving key
103+
// order when decoding a map. We decode our map to determine its length, create a dummy
104+
// list the same length as our map to determine the header size, and then decode each
105+
// key/value pair individually
106106
var tmpData map[RawMessageStr]RawMessageStr
107107
if err := cborUnmarshal(data, &tmpData); err != nil {
108108
return nil, err
109109
}
110+
// Create dummy list of same length to determine map header length
111+
tmpList := make([]bool, len(tmpData))
112+
tmpListRaw, err := cborMarshal(tmpList)
113+
if err != nil {
114+
return nil, err
115+
}
116+
tmpListHeader := tmpListRaw[0 : len(tmpListRaw)-len(tmpData)]
117+
// Strip off map header bytes
118+
data = data[len(tmpListHeader):]
110119
pairs := make([][2]PlutusData, 0, len(tmpData))
111-
for k, v := range tmpData {
112-
tmpKey, err := decodeCborRaw(k.Bytes())
120+
var rawKey, rawVal cbor.RawMessage
121+
// Read key/value pairs until we have no data left
122+
for len(data) > 0 {
123+
// Read raw key/value bytes
124+
data, err = cbor.UnmarshalFirst(data, &rawKey)
125+
if err != nil {
126+
return nil, err
127+
}
128+
data, err = cbor.UnmarshalFirst(data, &rawVal)
129+
if err != nil {
130+
return nil, err
131+
}
132+
// Decode key/value
133+
tmpKey, err := decodeCborRaw(rawKey)
113134
if err != nil {
114135
return nil, err
115136
}
116137
tmpKeyPd, err := decodeRaw(tmpKey)
117138
if err != nil {
118139
return nil, err
119140
}
120-
tmpVal, err := decodeCborRaw(v.Bytes())
141+
tmpVal, err := decodeCborRaw(rawVal)
121142
if err != nil {
122143
return nil, err
123144
}

data/encode.go

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,7 @@ func Encode(pd PlutusData) ([]byte, error) {
2323
func cborMarshal(data any) ([]byte, error) {
2424
buf := bytes.NewBuffer(nil)
2525
opts := cbor.EncOptions{
26-
// Make sure that maps have ordered keys
27-
Sort: cbor.SortCoreDeterministic,
26+
// NOTE: set any additional encoder options here
2827
}
2928
em, err := opts.EncMode()
3029
if err != nil {
@@ -125,9 +124,12 @@ func encodeConstr(c *Constr) (any, error) {
125124

126125
// encodeMap encodes a Map to CBOR map format.
127126
func encodeMap(m *Map) (any, error) {
128-
// Convert to map[any]any for CBOR encoding
129-
result := make(map[any]any, len(m.Pairs))
130-
127+
// The below is a hack to work around our CBOR library not supporting encoding a map
128+
// with a specific key order. We pre-encode each key/value pair, build a dummy list to
129+
// steal and modify its header, and build our own output from pieces. This avoids
130+
// needing to support 6 different possible encodings of a map's header byte depending
131+
// on length
132+
tmpPairs := make([][]byte, 0, len(m.Pairs))
131133
for _, pair := range m.Pairs {
132134
key, err := encodeToRaw(pair[0])
133135
if err != nil {
@@ -141,10 +143,29 @@ func encodeMap(m *Map) (any, error) {
141143
if err != nil {
142144
return nil, fmt.Errorf("failed to encode map value: %w", err)
143145
}
144-
result[RawMessageStr(string(keyRaw))] = value
146+
valueRaw, err := cborMarshal(value)
147+
if err != nil {
148+
return nil, fmt.Errorf("encode map value: %w", err)
149+
}
150+
tmpPairs = append(
151+
tmpPairs,
152+
slices.Concat(keyRaw, valueRaw),
153+
)
145154
}
146-
147-
return result, nil
155+
// Create dummy list with simple (one-byte) values so we can easily extract the header
156+
tmpList := make([]bool, len(tmpPairs))
157+
tmpListRaw, err := cborMarshal(tmpList)
158+
if err != nil {
159+
return nil, err
160+
}
161+
tmpListHeader := tmpListRaw[0 : len(tmpListRaw)-len(tmpPairs)]
162+
// Modify header byte to switch type from array to map
163+
tmpListHeader[0] |= 0x20
164+
// Build return value
165+
ret := bytes.NewBuffer(nil)
166+
_, _ = ret.Write(tmpListHeader)
167+
_, _ = ret.Write(slices.Concat(tmpPairs...))
168+
return cbor.RawMessage(ret.Bytes()), nil
148169
}
149170

150171
// encodeInteger encodes an Integer to CBOR format.

0 commit comments

Comments
 (0)