Skip to content

Commit 01f9712

Browse files
authored
Merge pull request kubernetes#125419 from benluddy/cbor-byteslice-base64
KEP-4222: Enable JSON-compatible base64 encoding of []byte for CBOR.
2 parents 10e3ec8 + 38f87df commit 01f9712

File tree

7 files changed

+151
-30
lines changed

7 files changed

+151
-30
lines changed

pkg/api/testing/unstructured_test.go

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -150,30 +150,6 @@ func TestRoundtripToUnstructured(t *testing.T) {
150150
// These are GVKs that whose CBOR roundtrippability is blocked by a known issue that must be
151151
// resolved as a prerequisite for alpha.
152152
knownFailureReasons := map[string][]schema.GroupVersionKind{
153-
// Since JSON cannot directly represent arbitrary byte sequences, a byte slice
154-
// encodes to a JSON string containing the base64 encoding of the slice
155-
// contents. Decoding a JSON string into a byte slice assumes (and requires) that
156-
// the JSON string contain base64-encoded data. The CBOR serializer must be
157-
// compatible with this behavior.
158-
"byte slices should be represented in unstructured as base64-encoded strings": {
159-
{Version: "v1", Kind: "Secret"},
160-
{Version: "v1", Kind: "SecretList"},
161-
{Version: "v1", Kind: "RangeAllocation"},
162-
{Version: "v1", Kind: "ConfigMap"},
163-
{Version: "v1", Kind: "ConfigMapList"},
164-
{Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "MutatingWebhookConfiguration"},
165-
{Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "MutatingWebhookConfigurationList"},
166-
{Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfiguration"},
167-
{Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfigurationList"},
168-
{Group: "admissionregistration.k8s.io", Version: "v1", Kind: "MutatingWebhookConfiguration"},
169-
{Group: "admissionregistration.k8s.io", Version: "v1", Kind: "MutatingWebhookConfigurationList"},
170-
{Group: "admissionregistration.k8s.io", Version: "v1", Kind: "ValidatingWebhookConfiguration"},
171-
{Group: "admissionregistration.k8s.io", Version: "v1", Kind: "ValidatingWebhookConfigurationList"},
172-
{Group: "certificates.k8s.io", Version: "v1beta1", Kind: "CertificateSigningRequest"},
173-
{Group: "certificates.k8s.io", Version: "v1beta1", Kind: "CertificateSigningRequestList"},
174-
{Group: "certificates.k8s.io", Version: "v1", Kind: "CertificateSigningRequest"},
175-
{Group: "certificates.k8s.io", Version: "v1", Kind: "CertificateSigningRequestList"},
176-
},
177153
// If a RawExtension's bytes are invalid JSON, its containing object can't be encoded to JSON.
178154
"rawextension needs to work in programs that assume json": {
179155
{Version: "v1", Kind: "List"},

staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/appendixa_test.go

Lines changed: 4 additions & 4 deletions
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
{

staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/decode.go

Lines changed: 12 additions & 2 deletions
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,

staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/decode_test.go

Lines changed: 40 additions & 0 deletions
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{

staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/encode.go

Lines changed: 8 additions & 0 deletions
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)

staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/encode_test.go

Lines changed: 12 additions & 0 deletions
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 {

staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/roundtrip_test.go

Lines changed: 75 additions & 0 deletions
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)