Skip to content

Commit 541e437

Browse files
benluddyk8s-publishing-bot
authored andcommitted
Enable JSON-compatible base64 encoding of []byte for CBOR.
The encoding/json package marshals []byte to a JSON string containing the base64 encoding of the input slice's bytes, and unmarshals JSON strings to []byte by assuming the JSON string contains a valid base64 text. As a binary format, CBOR is capable of representing arbitrary byte sequences without converting them to a text encoding, but it also needs to interoperate with the existing JSON serializer. It does this using the "expected later encoding" tags defined in RFC 8949, which indicate a specific text encoding to be used when interoperating with text-based protocols. The actual conversion to or from a text encoding is deferred until necessary, so no conversion is performed during roundtrips of []byte to CBOR. Kubernetes-commit: 38f87df0e31efac2742382c2ddd51b8d5276978f
1 parent c225984 commit 541e437

File tree

6 files changed

+151
-6
lines changed

6 files changed

+151
-6
lines changed

pkg/runtime/serializer/cbor/internal/modes/appendixa_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -286,11 +286,11 @@ func TestAppendixA(t *testing.T) {
286286
},
287287
},
288288
{
289-
example: hex("d74401020304"),
290-
decoded: "\x01\x02\x03\x04",
291-
encoded: hex("4401020304"),
289+
example: hex("d74401020304"), // 23(h'01020304')
290+
decoded: "01020304",
291+
encoded: hex("483031303230333034"), // '01020304'
292292
reasons: []string{
293-
reasonTagIgnored,
293+
"decoding a byte string enclosed in an expected later encoding tag into an interface{} value automatically converts to the specified encoding for JSON interoperability",
294294
},
295295
},
296296
{

pkg/runtime/serializer/cbor/internal/modes/decode.go

+12-2
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,12 @@ var Decode cbor.DecMode = func() cbor.DecMode {
9797
// Produce string concrete values when decoding a CBOR byte string into interface{}.
9898
DefaultByteStringType: reflect.TypeOf(""),
9999

100-
// Allow CBOR byte strings to be decoded into string destination values.
101-
ByteStringToString: cbor.ByteStringToStringAllowed,
100+
// Allow CBOR byte strings to be decoded into string destination values. If a byte
101+
// string is enclosed in an "expected later encoding" tag
102+
// (https://www.rfc-editor.org/rfc/rfc8949.html#section-3.4.5.2), then the text
103+
// encoding indicated by that tag (e.g. base64) will be applied to the contents of
104+
// the byte string.
105+
ByteStringToString: cbor.ByteStringToStringAllowedWithExpectedLaterEncoding,
102106

103107
// Allow CBOR byte strings to match struct fields when appearing as a map key.
104108
FieldNameByteString: cbor.FieldNameByteStringAllowed,
@@ -119,6 +123,12 @@ var Decode cbor.DecMode = func() cbor.DecMode {
119123
NaN: cbor.NaNDecodeForbidden,
120124
Inf: cbor.InfDecodeForbidden,
121125

126+
// When unmarshaling a byte string into a []byte, assume that the byte string
127+
// contains base64-encoded bytes, unless explicitly counterindicated by an "expected
128+
// later encoding" tag. This is consistent with the because of unmarshaling a JSON
129+
// text into a []byte.
130+
ByteStringExpectedFormat: cbor.ByteStringExpectedBase64,
131+
122132
// Reject the arbitrary-precision integer tags because they can't be faithfully
123133
// roundtripped through the allowable Unstructured types.
124134
BignumTag: cbor.BignumTagForbidden,

pkg/runtime/serializer/cbor/internal/modes/decode_test.go

+40
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,46 @@ func TestDecode(t *testing.T) {
163163
want: "",
164164
assertOnError: assertNilError,
165165
},
166+
{
167+
name: "byte string into []byte assumes base64",
168+
in: []byte("\x48AQIDBA=="), // 'AQIDBA=='
169+
into: []byte{},
170+
want: []byte{0x01, 0x02, 0x03, 0x04},
171+
assertOnError: assertNilError,
172+
},
173+
{
174+
name: "byte string into []byte errors on invalid base64",
175+
in: hex("41ff"), // h'ff'
176+
into: []byte{},
177+
assertOnError: assertErrorMessage("cbor: failed to decode base64 from byte string: illegal base64 data at input byte 0"),
178+
},
179+
{
180+
name: "empty byte string into []byte assumes base64",
181+
in: hex("40"), // ''
182+
into: []byte{},
183+
want: []byte{},
184+
assertOnError: assertNilError,
185+
},
186+
{
187+
name: "byte string with expected encoding tag into []byte does not convert",
188+
in: hex("d64401020304"), // 22(h'01020304')
189+
into: []byte{},
190+
want: []byte{0x01, 0x02, 0x03, 0x04},
191+
assertOnError: assertNilError,
192+
},
193+
{
194+
name: "byte string with expected encoding tag into string converts",
195+
in: hex("d64401020304"), // 22(h'01020304')
196+
into: "",
197+
want: "AQIDBA==",
198+
assertOnError: assertNilError,
199+
},
200+
{
201+
name: "byte string with expected encoding tag into interface{} converts",
202+
in: hex("d64401020304"), // 22(h'01020304')
203+
want: "AQIDBA==",
204+
assertOnError: assertNilError,
205+
},
166206
})
167207

168208
group(t, "text string", []test{

pkg/runtime/serializer/cbor/internal/modes/encode.go

+8
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,14 @@ var Encode cbor.EncMode = func() cbor.EncMode {
7979
// Marshal Go byte arrays to CBOR arrays of integers (as in JSON) instead of byte
8080
// strings.
8181
ByteArray: cbor.ByteArrayToArray,
82+
83+
// Marshal []byte to CBOR byte string enclosed in tag 22 (expected later base64
84+
// encoding, https://www.rfc-editor.org/rfc/rfc8949.html#section-3.4.5.2), to
85+
// interoperate with the existing JSON behavior. This indicates to the decoder that,
86+
// when decoding into a string (or unstructured), the resulting value should be the
87+
// base64 encoding of the original bytes. No base64 encoding or decoding needs to be
88+
// performed for []byte-to-CBOR-to-[]byte roundtrips.
89+
ByteSliceLaterFormat: cbor.ByteSliceLaterFormatBase64,
8290
}.EncMode()
8391
if err != nil {
8492
panic(err)

pkg/runtime/serializer/cbor/internal/modes/encode_test.go

+12
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,18 @@ func TestEncode(t *testing.T) {
7070
want: []byte{0x83, 0x01, 0x02, 0x03}, // [1, 2, 3]
7171
assertOnError: assertNilError,
7272
},
73+
{
74+
name: "string marshalled to byte string",
75+
in: "hello",
76+
want: []byte{0x45, 'h', 'e', 'l', 'l', 'o'},
77+
assertOnError: assertNilError,
78+
},
79+
{
80+
name: "[]byte marshalled to byte string in expected base64 encoding tag",
81+
in: []byte("hello"),
82+
want: []byte{0xd6, 0x45, 'h', 'e', 'l', 'l', 'o'},
83+
assertOnError: assertNilError,
84+
},
7385
} {
7486
encModes := tc.modes
7587
if len(encModes) == 0 {

pkg/runtime/serializer/cbor/internal/modes/roundtrip_test.go

+75
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package modes_test
1818

1919
import (
20+
"encoding/base64"
2021
"fmt"
2122
"math"
2223
"reflect"
@@ -340,3 +341,77 @@ func TestRoundtrip(t *testing.T) {
340341
}
341342
}
342343
}
344+
345+
// TestRoundtripTextEncoding exercises roundtrips between []byte and string.
346+
func TestRoundtripTextEncoding(t *testing.T) {
347+
for _, encMode := range allEncModes {
348+
for _, decMode := range allDecModes {
349+
t.Run(fmt.Sprintf("enc=%s/dec=%s/byte slice", encModeNames[encMode], decModeNames[decMode]), func(t *testing.T) {
350+
original := []byte("foo")
351+
352+
c, err := encMode.Marshal(original)
353+
if err != nil {
354+
t.Fatal(err)
355+
}
356+
357+
var unstructured interface{}
358+
if err := decMode.Unmarshal(c, &unstructured); err != nil {
359+
t.Fatal(err)
360+
}
361+
if diff := cmp.Diff(base64.StdEncoding.EncodeToString(original), unstructured); diff != "" {
362+
t.Errorf("[]byte to interface{}: unexpected diff:\n%s", diff)
363+
}
364+
365+
var s string
366+
if err := decMode.Unmarshal(c, &s); err != nil {
367+
t.Fatal(err)
368+
}
369+
if diff := cmp.Diff(base64.StdEncoding.EncodeToString(original), s); diff != "" {
370+
t.Errorf("[]byte to string: unexpected diff:\n%s", diff)
371+
}
372+
373+
var final []byte
374+
if err := decMode.Unmarshal(c, &final); err != nil {
375+
t.Fatal(err)
376+
}
377+
if diff := cmp.Diff(original, final); diff != "" {
378+
t.Errorf("[]byte to []byte: unexpected diff:\n%s", diff)
379+
}
380+
})
381+
382+
t.Run(fmt.Sprintf("enc=%s/dec=%s/string", encModeNames[encMode], decModeNames[decMode]), func(t *testing.T) {
383+
decoded := "foo"
384+
original := base64.StdEncoding.EncodeToString([]byte(decoded)) // "Zm9v"
385+
386+
c, err := encMode.Marshal(original)
387+
if err != nil {
388+
t.Fatal(err)
389+
}
390+
391+
var unstructured interface{}
392+
if err := decMode.Unmarshal(c, &unstructured); err != nil {
393+
t.Fatal(err)
394+
}
395+
if diff := cmp.Diff(original, unstructured); diff != "" {
396+
t.Errorf("string to interface{}: unexpected diff:\n%s", diff)
397+
}
398+
399+
var b []byte
400+
if err := decMode.Unmarshal(c, &b); err != nil {
401+
t.Fatal(err)
402+
}
403+
if diff := cmp.Diff([]byte(decoded), b); diff != "" {
404+
t.Errorf("string to []byte: unexpected diff:\n%s", diff)
405+
}
406+
407+
var final string
408+
if err := decMode.Unmarshal(c, &final); err != nil {
409+
t.Fatal(err)
410+
}
411+
if diff := cmp.Diff(original, final); diff != "" {
412+
t.Errorf("string to string: unexpected diff:\n%s", diff)
413+
}
414+
})
415+
}
416+
}
417+
}

0 commit comments

Comments
 (0)