-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcbor.go
More file actions
313 lines (275 loc) · 9.46 KB
/
Copy pathcbor.go
File metadata and controls
313 lines (275 loc) · 9.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
// MIT License
//
// # Copyright (c) 2026 Christopher J Bearman
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
package openprinttag
import (
"bytes"
"cmp"
"fmt"
"reflect"
"slices"
"strconv"
"strings"
"github.com/cjbearman/openprinttag/structtags"
"github.com/fxamacker/cbor/v2"
"github.com/x448/float16"
)
// maxRegionSize is set at 512 in the spec
const maxRegionSize = 512
const (
emptyDefiniteMap = byte(0xa0)
emptyIndefiniteMapByte1 = byte(0xbf)
emptyIndefiniteMapByte2 = byte(0xff)
)
type reflectionMapItem struct {
field *reflect.Value
name string
key int
optTags map[string]string
}
// encodeToCBOR encodes a specific region in cbor
// Depending on encoding options, we use definite or indefinite form for containers
func encodeToCBOR(r Region) (data []byte, err error) {
if r.RegionOptions().cborContainerType == CBORContainerTypeDefinite {
data, err = encodeAsDefiniteMap(r)
} else {
// CBORContainerTypeIndefinite or CBORContainerTypeAuto
data, err = encodeAsIndefiniteMap(r)
if len(data) == 2 {
if data[0] == emptyIndefiniteMapByte1 && data[1] == emptyIndefiniteMapByte2 {
// Well that is an empy indefinite container
if r.RegionOptions().cborContainerType == CBORContainerTypeAuto {
// With type auto, whilst we use indefinite, we have a free hand
// to optimize it to definite
data = []byte{emptyDefiniteMap}
}
}
}
}
if len(data) > maxRegionSize {
err = fmt.Errorf("region %s size of %d exceeds maximum permissable size of %d bytes", r.getRegionName(), len(data), maxRegionSize)
}
return
}
// encodeAsIndefiniteMap will encode a region using an indefinite map
func encodeAsIndefiniteMap(r Region) ([]byte, error) {
encmode, err := cbor.EncOptions{ShortestFloat: cbor.ShortestFloat16}.EncMode()
if err != nil {
return nil, err
}
var buf bytes.Buffer
enc := encmode.NewEncoder(&buf)
internal := reflect.ValueOf(r.getInternal()).Elem()
theMap := mapRegion(&internal)
if err = enc.StartIndefiniteMap(); err != nil {
return nil, err
}
for _, info := range getSortedFieldsToEncode(theMap) {
if err = enc.Encode(info.key); err != nil {
return nil, err
}
// Now encode the value depending on its type
kind := info.field.Elem().Type().Kind()
switch kind {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
if err = enc.Encode(info.field.Elem().Interface()); err != nil {
return nil, err
}
case reflect.Float32, reflect.Float64:
if err = enc.Encode(compressFloat(info.field.Elem().Interface(), r.RegionOptions())); err != nil {
return nil, err
}
case reflect.String:
if err = enc.Encode(info.field.Elem().Interface()); err != nil {
return nil, err
}
case reflect.Slice, reflect.Array:
kind := info.field.Elem().Type().Elem().Kind()
if kind == reflect.Uint8 {
// This is a []byte and must be encoded as a byte string
if err = enc.Encode(info.field.Elem().Interface()); err != nil {
return nil, err
}
} else {
// This is a []..something else and must be encoded as an array
switch info.optTags[structtags.OptTagContainerType] {
case structtags.OptTagContainerTypeDefinite:
// OPT tag designates definite encoding
err = enc.Encode(info.field.Elem().Interface())
default:
// Opt tag does not specify encoding or designates indefinite
// This is our default position based on historical encoding choices but should probably be changed to definite at some point
if err = enc.StartIndefiniteArray(); err != nil {
return nil, err
}
for n := 0; n < info.field.Elem().Len(); n++ {
sliceContent := info.field.Elem().Index(n).Interface()
if err = enc.Encode(sliceContent); err != nil {
return nil, err
}
}
if err = enc.EndIndefinite(); err != nil {
return nil, err
}
}
}
case reflect.Map:
// There is no use case for this currently
panic("Maps not supported within open print tag regions")
default:
return nil, fmt.Errorf("cannot encode %s", kind)
}
}
// Add any unknowns back in
for key, value := range r.GetUnknownFields() {
if err = enc.Encode(key); err != nil {
return nil, err
}
if err = enc.Encode(value); err != nil {
return nil, err
}
}
if err = enc.EndIndefinite(); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// encodeAsDefiniteMap will encode a region using an definite map
func encodeAsDefiniteMap(r Region) ([]byte, error) {
encmode, err := cbor.EncOptions{ShortestFloat: cbor.ShortestFloat16}.EncMode()
if err != nil {
return nil, err
}
var buf bytes.Buffer
enc := encmode.NewEncoder(&buf)
internal := reflect.ValueOf(r.getInternal()).Elem()
theMap := mapRegion(&internal)
mapToEncode := make(map[any]any)
for _, info := range getSortedFieldsToEncode(theMap) {
if info.field.Elem().Type().Kind() == reflect.Float32 {
mapToEncode[info.key] = compressFloat(info.field.Elem().Interface(), r.RegionOptions())
} else {
mapToEncode[info.key] = info.field.Elem().Interface()
}
}
// Add any unknowns back in
for key, value := range r.GetUnknownFields() {
mapToEncode[key] = value
}
if err = enc.Encode(mapToEncode); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// getSortedFieldsToEncode takes our map of the region fields
// and returns just those that are not nil in strict order
// of key. This helps keep our encoded cbor easy to debug
func getSortedFieldsToEncode(theMap map[int]reflectionMapItem) (arr []reflectionMapItem) {
for _, item := range theMap {
if !item.field.IsNil() {
arr = append(arr, item)
}
}
slices.SortFunc(arr, func(a, b reflectionMapItem) int {
return cmp.Compare(a.key, b.key)
})
return
}
// mapRegion will take a region's internal structure
// and use it to generate a map containing the cbor fields
func mapRegion(internal *reflect.Value) map[int]reflectionMapItem {
theMap := make(map[int]reflectionMapItem, internal.NumField())
for i := 0; i < internal.NumField(); i++ {
field := internal.FieldByIndex([]int{i})
name := internal.Type().Field(i).Name
cborTag := internal.Type().Field(i).Tag.Get("cbor")
if cborTag == "" {
// Not a field of interest since it has no cbor tag
continue
}
cborTagBits := strings.Split(cborTag, ",")
// The first bit of the tag should be the integer key, or -
if cborTagBits[0] == "-" {
// Field skipped from cbor representation
continue
}
key, err := strconv.Atoi(cborTagBits[0])
if err != nil {
panic(fmt.Sprintf("not a valid integer key at start of cbor tag on field %s", name))
}
theMap[key] = reflectionMapItem{
field: &field,
name: name,
key: key,
optTags: structtags.ReadOptTags(internal.Type().Field(i).Tag),
}
}
return theMap
}
// compressFloat attempts to comrpess a float32 down to an integer
// where possible
// cbor library will already compress to float16 where a lossless
// downsizing is possible
func compressFloat(orig any, opts *RegionOptions) any {
// the original will be either a float32 or float64
// Start by upconverting to float64 if we can
var original float64
if f64, ok := orig.(float64); ok {
original = f64
} else if f32, ok := orig.(float32); ok {
original = float64(f32)
} else {
panic("not a valid float")
}
// We do our best to optimize the number down to the smallest possible representation without loss, starting with integers
// If we can downconvert with no loss, then that's our best choice
// Is the number negative
if original < 0 {
// It is negative, so try first with signed integers
if float64(int8(original)) == original {
return int8(original)
}
if float64(int16(original)) == original {
return int16(original)
}
}
// The number is positive, so do that again with unsigned integers
if float64(uint8(original)) == original {
return uint8(original)
}
if float64(uint16(original)) == original {
return uint16(original)
}
// AN integer doesn't work, so let's go float. Depending on encoding options, we may be able to downconvert to float16 or float32 without loss
// It doesn't matter that we've upscaled to a float64, because
// the cbor library automatically optimizes to the smallest usable
// float size without loss
// However, depending on encode options we can do a lossy downscale
switch opts.GetFloatMaxPrecision() {
case FloatMaxPrecision16:
return float16.Fromfloat32(float32(original)).Float32()
case FloatMaxPrecision32:
return float32(original)
default:
return original
}
}