Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 116 additions & 2 deletions encoding/cbor.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2023-2024 Contributors to the Veraison project.
// Copyright 2023-2025 Contributors to the Veraison project.
// SPDX-License-Identifier: Apache-2.0

package encoding
Expand All @@ -9,6 +9,7 @@ import (
"fmt"
"math"
"reflect"
"sort"
"strconv"
"strings"

Expand Down Expand Up @@ -49,6 +50,13 @@ func doSerializeStructToCBOR(
continue
}

_, ok := typeField.Tag.Lookup("field-cache")
if ok {
if err := addCachedFieldsToMapCBOR(em, valField, rawMap); err != nil {
return err
}
}

tag, ok := typeField.Tag.Lookup("cbor")
if !ok {
continue
Expand Down Expand Up @@ -129,6 +137,7 @@ func doPopulateStructFromCBOR(
}

var embeds []embedded
var fieldCache reflect.Value

for i := 0; i < structVal.NumField(); i++ {
typeField := structType.Field(i)
Expand All @@ -138,6 +147,12 @@ func doPopulateStructFromCBOR(
continue
}

_, ok := typeField.Tag.Lookup("field-cache")
if ok {
fieldCache = valField
continue
}

tag, ok := typeField.Tag.Lookup("cbor")
if !ok {
continue
Expand Down Expand Up @@ -192,7 +207,9 @@ func doPopulateStructFromCBOR(
}
}

return nil
// Any remaining contents of rawMap will be added to the field cache,
// if current struct has one.
return updateFieldCacheCBOR(dm, fieldCache, rawMap)
}

// structFieldsCBOR is a specialized implementation of "OrderedMap", where the
Expand Down Expand Up @@ -269,6 +286,7 @@ func (o *structFieldsCBOR) ToCBOR(em cbor.EncMode) ([]byte, error) {
return nil, errors.New("mapLen cannot exceed math.MaxUint32")
}

lexSort(em, o.Keys)
for _, key := range o.Keys {
marshalledKey, err := em.Marshal(key)
if err != nil {
Expand Down Expand Up @@ -414,3 +432,99 @@ func processAdditionalInfo(

return mapLen, rest, nil
}

func addCachedFieldsToMapCBOR(em cbor.EncMode, cacheField reflect.Value, rawMap *structFieldsCBOR) error {
if !isMapStringAny(cacheField) {
return errors.New("field-cache does not appear to be a map[string]any")
}

if !cacheField.IsValid() || cacheField.IsNil() {
// field cache was never set, so nothing to do
return nil
}

for _, key := range cacheField.MapKeys() {
keyText := key.String()
keyInt, err := strconv.Atoi(keyText)
if err != nil {
return fmt.Errorf(
"cached field name not an integer (cannot encode to CBOR): %s",
keyText,
)
}

data, err := em.Marshal(cacheField.MapIndex(key).Interface())
if err != nil {
return fmt.Errorf(
"error marshaling field-cache entry %q: %w",
keyText,
err,
)
}

if err := rawMap.Add(keyInt, cbor.RawMessage(data)); err != nil {
return fmt.Errorf(
"could not add field-cache entry %q to serialization map: %w",
keyText,
err,
)
}
}

return nil
}

func updateFieldCacheCBOR(dm cbor.DecMode, cacheField reflect.Value, rawMap *structFieldsCBOR) error {
if !cacheField.IsValid() {
// current struct does not have a field-cache field
return nil
}

if !isMapStringAny(cacheField) {
return errors.New("field-cache does not appear to be a map[string]any")
}

if cacheField.IsNil() {
cacheField.Set(reflect.MakeMap(cacheField.Type()))
}

for key, rawVal := range rawMap.Fields {
var val any
if err := dm.Unmarshal(rawVal, &val); err != nil {
return fmt.Errorf("could not unmarshal key %d: %w", key, err)
}

keyText := fmt.Sprint(key)
keyVal := reflect.ValueOf(keyText)
valVal := reflect.ValueOf(val)
cacheField.SetMapIndex(keyVal, valVal)
}

return nil
}

// Lexicographic sorting of CBOR integer keys. See:
// https://www.ietf.org/archive/id/draft-ietf-cbor-cde-13.html#name-the-lexicographic-map-sorti
func lexSort(em cbor.EncMode, v []int) {
sort.Slice(v, func(i, j int) bool {
a, err := em.Marshal(v[i])
if err != nil {
panic(err) // integer encoding cannot fail
}

b, err := em.Marshal(v[j])
if err != nil {
panic(err) // integer encoding cannot fail
}

for k, v := range a {
if v < b[k] {
return true
} else if v > b[k] {
return false
}
}

return false
})
}
79 changes: 78 additions & 1 deletion encoding/cbor_test.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2021-2024 Contributors to the Veraison project.
// Copyright 2021-2025 Contributors to the Veraison project.
// SPDX-License-Identifier: Apache-2.0
package encoding

Expand Down Expand Up @@ -107,6 +107,41 @@ func Test_PopulateStructFromCBOR_simple(t *testing.T) {

}

func Test_SerializeStructToCBOR_cde_ordering(t *testing.T) {
val := struct {
Field8 int `cbor:"8,keyasint"`
FieldN3 int `cbor:"-3,keyasint"`
Field0 int `cbor:"0,keyasint"`
FieldN1 int `cbor:"-1,keyasint"`
}{
Field8: 1,
FieldN3: 3,
Field0: 0,
FieldN1: 2,
}

expected := []byte{
0xa4, // map(4)

0x00, // key: 0
0x00, // value: 0

0x08, // key: 8
0x01, // value: 1

0x20, // key: -1
0x02, // value: 2

0x22, // key: -3
0x03, // value: 3
}

em := mustInitEncMode()
data, err := SerializeStructToCBOR(em, val)
assert.NoError(t, err)
assert.Equal(t, expected, data)
}

func Test_structFieldsCBOR_CRUD(t *testing.T) {
sf := newStructFieldsCBOR()

Expand Down Expand Up @@ -278,3 +313,45 @@ func Test_processAdditionalInfo(t *testing.T) {
_, _, err = processAdditionalInfo(addInfo, []byte{})
assert.EqualError(t, err, "unexpected EOF")
}

func Test_lexSort(t *testing.T) {
test_cases := []struct {
title string
input []int
expected []int
}{
{
title: "non-negative",
input: []int{1, 4, 0, 2, 3},
expected: []int{0, 1, 2, 3, 4},
},
{
title: "negative",
input: []int{-1, -4, -2, -3},
expected: []int{-1, -2, -3, -4},
},
{
title: "mixed",
input: []int{-1, 0, 3, 1, -2},
expected: []int{0, 1, 3, -1, -2},
},
{
title: "already sorted",
input: []int{0, 1, 3, -1, -2},
expected: []int{0, 1, 3, -1, -2},
},
{
title: "different length encoding",
input: []int{65535, 256},
expected: []int{256, 65535},
},
}

for _, tc := range test_cases {
em := mustInitEncMode()
t.Run(tc.title, func(t *testing.T) {
lexSort(em, tc.input)
assert.Equal(t, tc.expected, tc.input)
})
}
}
20 changes: 19 additions & 1 deletion encoding/embedded.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2024 Contributors to the Veraison project.
// Copyright 2024-2025 Contributors to the Veraison project.
// SPDX-License-Identifier: Apache-2.0
package encoding

Expand Down Expand Up @@ -49,3 +49,21 @@ func collectEmbedded(

return false
}

// isMapStringAny returns true iff the provided value, v, is of type
// map[string]any.
func isMapStringAny(v reflect.Value) bool {
if v.Kind() != reflect.Map {
return false
}

if v.Type().Key().Kind() != reflect.String {
return false
}

if v.Type().Elem().Kind() != reflect.Interface {
return false
}

return true
}
Loading