From 066421f108c2d6e53c84387ade80bb97a5c819a8 Mon Sep 17 00:00:00 2001 From: Ben Luddy Date: Tue, 13 Feb 2024 11:03:24 -0500 Subject: [PATCH] Add CBOR Serializer implementation. --- .../pkg/runtime/serializer/cbor/cbor.go | 225 ++++++++- .../pkg/runtime/serializer/cbor/cbor_test.go | 475 ++++++++++++++++++ .../runtime/serializer/cbor/direct/direct.go | 36 ++ .../serializer/cbor/internal/modes/decode.go | 103 ++++ .../cbor/internal/modes/diagnostic.go | 36 ++ .../serializer/cbor/internal/modes/encode.go | 96 ++++ vendor/modules.txt | 1 + 7 files changed, 971 insertions(+), 1 deletion(-) create mode 100644 staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/cbor_test.go create mode 100644 staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct/direct.go create mode 100644 staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/decode.go create mode 100644 staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/diagnostic.go create mode 100644 staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/encode.go diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/cbor.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/cbor.go index c8176dca7b3..9fcf0f8421d 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/cbor.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/cbor.go @@ -17,5 +17,228 @@ limitations under the License. package cbor import ( - _ "github.com/fxamacker/cbor/v2" + "bytes" + "encoding/hex" + "errors" + "fmt" + "io" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes" + "k8s.io/apimachinery/pkg/runtime/serializer/recognizer" + util "k8s.io/apimachinery/pkg/util/runtime" + + "github.com/fxamacker/cbor/v2" ) + +type metaFactory interface { + // Interpret should return the version and kind of the wire-format of the object. + Interpret(data []byte) (*schema.GroupVersionKind, error) +} + +type defaultMetaFactory struct{} + +func (mf *defaultMetaFactory) Interpret(data []byte) (*schema.GroupVersionKind, error) { + var tm metav1.TypeMeta + // The input is expected to include additional map keys besides apiVersion and kind, so use + // lax mode for decoding into TypeMeta. + if err := modes.DecodeLax.Unmarshal(data, &tm); err != nil { + return nil, fmt.Errorf("unable to determine group/version/kind: %w", err) + } + actual := tm.GetObjectKind().GroupVersionKind() + return &actual, nil +} + +type Serializer interface { + runtime.Serializer + recognizer.RecognizingDecoder +} + +var _ Serializer = &serializer{} + +type options struct { + strict bool +} + +type Option func(*options) + +func Strict(s bool) Option { + return func(opts *options) { + opts.strict = s + } +} + +type serializer struct { + metaFactory metaFactory + creater runtime.ObjectCreater + typer runtime.ObjectTyper + options options +} + +func NewSerializer(creater runtime.ObjectCreater, typer runtime.ObjectTyper, options ...Option) Serializer { + return newSerializer(&defaultMetaFactory{}, creater, typer, options...) +} + +func newSerializer(metaFactory metaFactory, creater runtime.ObjectCreater, typer runtime.ObjectTyper, options ...Option) *serializer { + s := &serializer{ + metaFactory: metaFactory, + creater: creater, + typer: typer, + } + for _, o := range options { + o(&s.options) + } + return s +} + +func (s *serializer) Identifier() runtime.Identifier { + return "cbor" +} + +func (s *serializer) Encode(obj runtime.Object, w io.Writer) error { + if _, err := w.Write(selfDescribedCBOR); err != nil { + return err + } + + e := modes.Encode.NewEncoder(w) + if u, ok := obj.(runtime.Unstructured); ok { + return e.Encode(u.UnstructuredContent()) + } + return e.Encode(obj) +} + +// gvkWithDefaults returns group kind and version defaulting from provided default +func gvkWithDefaults(actual, defaultGVK schema.GroupVersionKind) schema.GroupVersionKind { + if len(actual.Kind) == 0 { + actual.Kind = defaultGVK.Kind + } + if len(actual.Version) == 0 && len(actual.Group) == 0 { + actual.Group = defaultGVK.Group + actual.Version = defaultGVK.Version + } + if len(actual.Version) == 0 && actual.Group == defaultGVK.Group { + actual.Version = defaultGVK.Version + } + return actual +} + +// diagnose returns the diagnostic encoding of a well-formed CBOR data item. +func diagnose(data []byte) string { + diag, err := modes.Diagnostic.Diagnose(data) + if err != nil { + // Since the input must already be well-formed CBOR, converting it to diagnostic + // notation should not fail. + util.HandleError(err) + + return hex.EncodeToString(data) + } + return diag +} + +func (s *serializer) unmarshal(data []byte, into interface{}) (strict, lax error) { + if u, ok := into.(runtime.Unstructured); ok { + var content map[string]interface{} + defer func() { + // TODO: The UnstructuredList implementation of SetUnstructuredContent is + // not identical to what unstructuredJSONScheme does: (1) it retains the + // "items" key in its Object field, and (2) it does not infer a singular + // Kind from the list's Kind and populate omitted apiVersion/kind for all + // entries in Items. + u.SetUnstructuredContent(content) + }() + into = &content + } + + if !s.options.strict { + return nil, modes.DecodeLax.Unmarshal(data, into) + } + + err := modes.Decode.Unmarshal(data, into) + // TODO: UnknownFieldError is ambiguous. It only provides the index of the first problematic + // map entry encountered and does not indicate which map the index refers to. + var unknownField *cbor.UnknownFieldError + if errors.As(err, &unknownField) { + // Unlike JSON, there are no strict errors in CBOR for duplicate map keys. CBOR maps + // with duplicate keys are considered invalid according to the spec and are rejected + // entirely. + return runtime.NewStrictDecodingError([]error{unknownField}), modes.DecodeLax.Unmarshal(data, into) + } + return nil, err +} + +func (s *serializer) Decode(data []byte, gvk *schema.GroupVersionKind, into runtime.Object) (runtime.Object, *schema.GroupVersionKind, error) { + // A preliminary pass over the input to obtain the actual GVK is redundant on a successful + // decode into Unstructured. + if _, ok := into.(runtime.Unstructured); ok { + if _, unmarshalErr := s.unmarshal(data, into); unmarshalErr != nil { + actual, interpretErr := s.metaFactory.Interpret(data) + if interpretErr != nil { + return nil, nil, interpretErr + } + + if gvk != nil { + *actual = gvkWithDefaults(*actual, *gvk) + } + + return nil, actual, unmarshalErr + } + + actual := into.GetObjectKind().GroupVersionKind() + if len(actual.Kind) == 0 { + return nil, &actual, runtime.NewMissingKindErr(diagnose(data)) + } + if len(actual.Version) == 0 { + return nil, &actual, runtime.NewMissingVersionErr(diagnose(data)) + } + + return into, &actual, nil + } + + actual, err := s.metaFactory.Interpret(data) + if err != nil { + return nil, nil, err + } + + if gvk != nil { + *actual = gvkWithDefaults(*actual, *gvk) + } + + if into != nil { + types, _, err := s.typer.ObjectKinds(into) + if err != nil { + return nil, actual, err + } + *actual = gvkWithDefaults(*actual, types[0]) + } + + if len(actual.Kind) == 0 { + return nil, actual, runtime.NewMissingKindErr(diagnose(data)) + } + if len(actual.Version) == 0 { + return nil, actual, runtime.NewMissingVersionErr(diagnose(data)) + } + + obj, err := runtime.UseOrCreateObject(s.typer, s.creater, *actual, into) + if err != nil { + return nil, actual, err + } + + strict, err := s.unmarshal(data, obj) + if err != nil { + return nil, actual, err + } + return obj, actual, strict +} + +// selfDescribedCBOR is the CBOR encoding of the head of tag number 55799. This tag, specified in +// RFC 8949 Section 3.4.6 "Self-Described CBOR", encloses all output from the encoder, has no +// special semantics, and is used as a magic number to recognize CBOR-encoded data items. +// +// See https://www.rfc-editor.org/rfc/rfc8949.html#name-self-described-cbor. +var selfDescribedCBOR = []byte{0xd9, 0xd9, 0xf7} + +func (s *serializer) RecognizesData(data []byte) (ok, unknown bool, err error) { + return bytes.HasPrefix(data, selfDescribedCBOR), false, nil +} diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/cbor_test.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/cbor_test.go new file mode 100644 index 00000000000..06b44ccf096 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/cbor_test.go @@ -0,0 +1,475 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// The tests in this package focus on the correctness of its implementation of +// runtime.Serializer. The specific behavior of marshaling Go values to CBOR bytes and back is +// tested in the ./internal/modes package, which is used both by the Serializer implementation and +// the package-scoped Marshal/Unmarshal functions in the ./direct package. +package cbor + +import ( + "bytes" + "encoding/hex" + "errors" + "io" + "reflect" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes" + + "github.com/google/go-cmp/cmp" +) + +func TestRecognizesData(t *testing.T) { + for _, tc := range []struct { + in []byte + recognizes bool + }{ + { + in: nil, + recognizes: false, + }, + { + in: []byte{}, + recognizes: false, + }, + { + in: []byte{0xd9}, + recognizes: false, + }, + { + in: []byte{0xd9, 0xd9}, + recognizes: false, + }, + { + in: []byte{0xd9, 0xd9, 0xf7}, + recognizes: true, + }, + { + in: []byte{0xff, 0xff, 0xff}, + recognizes: false, + }, + { + in: []byte{0xd9, 0xd9, 0xf7, 0x01, 0x02, 0x03}, + recognizes: true, + }, + { + in: []byte{0xff, 0xff, 0xff, 0x01, 0x02, 0x03}, + recognizes: false, + }, + } { + t.Run(hex.EncodeToString(tc.in), func(t *testing.T) { + s := NewSerializer(nil, nil) + recognizes, unknown, err := s.RecognizesData(tc.in) + if recognizes != tc.recognizes { + t.Errorf("expected recognized to be %t, got %t", tc.recognizes, recognizes) + } + if unknown { + t.Error("expected unknown to be false, got true") + } + if err != nil { + t.Errorf("expected nil error, got: %v", err) + } + }) + } +} + +type stubWriter struct { + n int + err error +} + +func (w stubWriter) Write([]byte) (int, error) { + return w.n, w.err +} + +// anyObject wraps arbitrary concrete values to be encoded or decoded. +type anyObject struct { + Value interface{} +} + +func (p anyObject) GetObjectKind() schema.ObjectKind { + return schema.EmptyObjectKind +} + +func (anyObject) DeepCopyObject() runtime.Object { + panic("unimplemented") +} + +func (p anyObject) MarshalCBOR() ([]byte, error) { + return modes.Encode.Marshal(p.Value) +} + +func (p *anyObject) UnmarshalCBOR(in []byte) error { + return modes.Decode.Unmarshal(in, &p.Value) +} + +func TestEncode(t *testing.T) { + for _, tc := range []struct { + name string + in runtime.Object + assertOnWriter func() (io.Writer, func(*testing.T)) + assertOnError func(*testing.T, error) + }{ + { + name: "io error writing self described cbor tag", + assertOnWriter: func() (io.Writer, func(*testing.T)) { + return stubWriter{err: io.ErrShortWrite}, func(*testing.T) {} + }, + assertOnError: func(t *testing.T, err error) { + if !errors.Is(err, io.ErrShortWrite) { + t.Errorf("expected io.ErrShortWrite, got: %v", err) + } + }, + }, + { + name: "output enclosed by self-described CBOR tag", + in: anyObject{}, + assertOnWriter: func() (io.Writer, func(*testing.T)) { + var b bytes.Buffer + return &b, func(t *testing.T) { + if !bytes.HasPrefix(b.Bytes(), []byte{0xd9, 0xd9, 0xf7}) { + t.Errorf("expected output to have prefix 0xd9d9f7: 0x%x", b.Bytes()) + } + } + }, + assertOnError: func(t *testing.T, err error) { + if err != nil { + t.Errorf("expected nil error, got: %v", err) + } + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + s := NewSerializer(nil, nil) + w, assertOnWriter := tc.assertOnWriter() + err := s.Encode(tc.in, w) + tc.assertOnError(t, err) + assertOnWriter(t) + }) + } +} + +func TestDecode(t *testing.T) { + for _, tc := range []struct { + name string + options []Option + data []byte + gvk *schema.GroupVersionKind + metaFactory metaFactory + typer runtime.ObjectTyper + creater runtime.ObjectCreater + into runtime.Object + expectedObj runtime.Object + expectedGVK *schema.GroupVersionKind + assertOnError func(*testing.T, error) + }{ + { + name: "error determining gvk", + metaFactory: stubMetaFactory{err: errors.New("test")}, + assertOnError: func(t *testing.T, err error) { + if err == nil || err.Error() != "test" { + t.Errorf("expected error \"test\", got: %v", err) + } + }, + }, + { + name: "typer does not recognize into", + gvk: &schema.GroupVersionKind{Group: "x", Version: "y", Kind: "z"}, + metaFactory: stubMetaFactory{gvk: &schema.GroupVersionKind{}}, + typer: notRegisteredTyper{}, + into: &anyObject{}, + expectedObj: nil, + expectedGVK: &schema.GroupVersionKind{Group: "x", Version: "y", Kind: "z"}, + assertOnError: func(t *testing.T, err error) { + if !runtime.IsNotRegisteredError(err) { + t.Errorf("expected NotRegisteredError, got: %v", err) + } + }, + }, + { + name: "gvk from type of into", + data: []byte{0xf6}, + gvk: &schema.GroupVersionKind{}, + metaFactory: stubMetaFactory{gvk: &schema.GroupVersionKind{}}, + typer: stubTyper{gvks: []schema.GroupVersionKind{{Group: "x", Version: "y", Kind: "z"}}}, + into: &anyObject{}, + expectedObj: &anyObject{}, + expectedGVK: &schema.GroupVersionKind{Group: "x", Version: "y", Kind: "z"}, + assertOnError: func(t *testing.T, err error) { + if err != nil { + t.Errorf("expected nil error, got: %v", err) + } + }, + }, + { + name: "strict mode strict error", + options: []Option{Strict(true)}, + data: []byte{0xa1, 0x61, 'z', 0x01}, // {'z': 1} + gvk: &schema.GroupVersionKind{}, + metaFactory: stubMetaFactory{gvk: &schema.GroupVersionKind{}}, + typer: stubTyper{gvks: []schema.GroupVersionKind{{Group: "x", Version: "y", Kind: "z"}}}, + into: &metav1.PartialObjectMetadata{}, + expectedObj: &metav1.PartialObjectMetadata{}, + expectedGVK: &schema.GroupVersionKind{Group: "x", Version: "y", Kind: "z"}, + assertOnError: func(t *testing.T, err error) { + if !runtime.IsStrictDecodingError(err) { + t.Errorf("expected StrictDecodingError, got: %v", err) + } + }, + }, + { + name: "no strict mode no strict error", + data: []byte{0xa1, 0x61, 'z', 0x01}, // {'z': 1} + gvk: &schema.GroupVersionKind{}, + metaFactory: stubMetaFactory{gvk: &schema.GroupVersionKind{}}, + typer: stubTyper{gvks: []schema.GroupVersionKind{{Group: "x", Version: "y", Kind: "z"}}}, + into: &metav1.PartialObjectMetadata{}, + expectedObj: &metav1.PartialObjectMetadata{}, + expectedGVK: &schema.GroupVersionKind{Group: "x", Version: "y", Kind: "z"}, + assertOnError: func(t *testing.T, err error) { + if err != nil { + t.Errorf("expected nil error, got: %v", err) + } + }, + }, + { + name: "unknown error from typer on into", + gvk: &schema.GroupVersionKind{}, + metaFactory: stubMetaFactory{gvk: &schema.GroupVersionKind{}}, + typer: stubTyper{err: errors.New("test")}, + into: &anyObject{}, + expectedObj: nil, + expectedGVK: &schema.GroupVersionKind{}, + assertOnError: func(t *testing.T, err error) { + if err == nil || err.Error() != "test" { + t.Errorf("expected error \"test\", got: %v", err) + } + }, + }, + { + name: "missing kind", + gvk: &schema.GroupVersionKind{Version: "v"}, + metaFactory: stubMetaFactory{gvk: &schema.GroupVersionKind{}}, + expectedObj: nil, + expectedGVK: &schema.GroupVersionKind{Version: "v"}, + assertOnError: func(t *testing.T, err error) { + if !runtime.IsMissingKind(err) { + t.Errorf("expected MissingKind, got: %v", err) + } + }, + }, + { + name: "missing version", + gvk: &schema.GroupVersionKind{Kind: "k"}, + metaFactory: stubMetaFactory{gvk: &schema.GroupVersionKind{}}, + expectedObj: nil, + expectedGVK: &schema.GroupVersionKind{Kind: "k"}, + assertOnError: func(t *testing.T, err error) { + if !runtime.IsMissingVersion(err) { + t.Errorf("expected MissingVersion, got: %v", err) + } + }, + }, + { + name: "creater error", + gvk: &schema.GroupVersionKind{Group: "x", Version: "y", Kind: "z"}, + metaFactory: stubMetaFactory{gvk: &schema.GroupVersionKind{}}, + creater: stubCreater{err: errors.New("test")}, + expectedObj: nil, + expectedGVK: &schema.GroupVersionKind{Group: "x", Version: "y", Kind: "z"}, + assertOnError: func(t *testing.T, err error) { + if err == nil || err.Error() != "test" { + t.Errorf("expected error \"test\", got: %v", err) + } + }, + }, + { + name: "unmarshal error", + data: nil, // EOF + gvk: &schema.GroupVersionKind{Group: "x", Version: "y", Kind: "z"}, + metaFactory: stubMetaFactory{gvk: &schema.GroupVersionKind{}}, + creater: stubCreater{obj: &anyObject{}}, + expectedObj: nil, + expectedGVK: &schema.GroupVersionKind{Group: "x", Version: "y", Kind: "z"}, + assertOnError: func(t *testing.T, err error) { + if !errors.Is(err, io.EOF) { + t.Errorf("expected EOF, got: %v", err) + } + }, + }, + { + name: "strict mode unmarshal error", + options: []Option{Strict(true)}, + data: nil, // EOF + gvk: &schema.GroupVersionKind{Group: "x", Version: "y", Kind: "z"}, + metaFactory: stubMetaFactory{gvk: &schema.GroupVersionKind{}}, + creater: stubCreater{obj: &anyObject{}}, + expectedObj: nil, + expectedGVK: &schema.GroupVersionKind{Group: "x", Version: "y", Kind: "z"}, + assertOnError: func(t *testing.T, err error) { + if !errors.Is(err, io.EOF) { + t.Errorf("expected EOF, got: %v", err) + } + }, + }, + { + name: "into unstructured unmarshal error", + data: nil, // EOF + gvk: &schema.GroupVersionKind{Group: "x", Version: "y", Kind: "z"}, + metaFactory: stubMetaFactory{gvk: &schema.GroupVersionKind{}}, + into: &unstructured.Unstructured{}, + expectedObj: nil, + expectedGVK: &schema.GroupVersionKind{Group: "x", Version: "y", Kind: "z"}, + assertOnError: func(t *testing.T, err error) { + if !errors.Is(err, io.EOF) { + t.Errorf("expected EOF, got: %v", err) + } + }, + }, + { + name: "into unstructured missing kind", + data: []byte("\xa1\x6aapiVersion\x61v"), + into: &unstructured.Unstructured{}, + expectedObj: nil, + expectedGVK: &schema.GroupVersionKind{Version: "v"}, + assertOnError: func(t *testing.T, err error) { + if !runtime.IsMissingKind(err) { + t.Errorf("expected MissingKind, got: %v", err) + } + }, + }, + { + name: "into unstructured missing version", + data: []byte("\xa1\x64kind\x61k"), + into: &unstructured.Unstructured{}, + expectedObj: nil, + expectedGVK: &schema.GroupVersionKind{Kind: "k"}, + assertOnError: func(t *testing.T, err error) { + if !runtime.IsMissingVersion(err) { + t.Errorf("expected MissingVersion, got: %v", err) + } + }, + }, + { + name: "into unstructured", + data: []byte("\xa2\x6aapiVersion\x61v\x64kind\x61k"), + into: &unstructured.Unstructured{}, + expectedObj: &unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "v", + "kind": "k", + }}, + expectedGVK: &schema.GroupVersionKind{Version: "v", Kind: "k"}, + assertOnError: func(t *testing.T, err error) { + if err != nil { + t.Errorf("expected nil error, got: %v", err) + } + }, + }, + { + name: "using unstructured creater", + data: []byte("\xa2\x6aapiVersion\x61v\x64kind\x61k"), + metaFactory: &defaultMetaFactory{}, + creater: stubCreater{obj: &unstructured.Unstructured{}}, + expectedObj: &unstructured.Unstructured{Object: map[string]interface{}{ + "apiVersion": "v", + "kind": "k", + }}, + expectedGVK: &schema.GroupVersionKind{Version: "v", Kind: "k"}, + assertOnError: func(t *testing.T, err error) { + if err != nil { + t.Errorf("expected nil error, got: %v", err) + } + }, + }, + } { + t.Run(tc.name, func(t *testing.T) { + s := newSerializer(tc.metaFactory, tc.creater, tc.typer, tc.options...) + + actualObj, actualGVK, err := s.Decode(tc.data, tc.gvk, tc.into) + tc.assertOnError(t, err) + + if !reflect.DeepEqual(tc.expectedObj, actualObj) { + t.Error(cmp.Diff(tc.expectedObj, actualObj)) + } + + if diff := cmp.Diff(tc.expectedGVK, actualGVK); diff != "" { + t.Error(diff) + } + }) + } +} + +func TestMetaFactoryInterpret(t *testing.T) { + mf := &defaultMetaFactory{} + _, err := mf.Interpret(nil) + if err == nil { + t.Error("expected non-nil error") + } + gvk, err := mf.Interpret([]byte("\xa2\x6aapiVersion\x63a/b\x64kind\x61c")) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if diff := cmp.Diff(&schema.GroupVersionKind{Group: "a", Version: "b", Kind: "c"}, gvk); diff != "" { + t.Error(diff) + } +} + +type stubTyper struct { + gvks []schema.GroupVersionKind + unversioned bool + err error +} + +func (t stubTyper) ObjectKinds(obj runtime.Object) ([]schema.GroupVersionKind, bool, error) { + return t.gvks, t.unversioned, t.err +} + +func (stubTyper) Recognizes(schema.GroupVersionKind) bool { + return false +} + +type stubCreater struct { + obj runtime.Object + err error +} + +func (c stubCreater) New(gvk schema.GroupVersionKind) (runtime.Object, error) { + return c.obj, c.err +} + +type notRegisteredTyper struct{} + +func (notRegisteredTyper) ObjectKinds(obj runtime.Object) ([]schema.GroupVersionKind, bool, error) { + return nil, false, runtime.NewNotRegisteredErrForType("test", reflect.TypeOf(obj)) +} + +func (notRegisteredTyper) Recognizes(schema.GroupVersionKind) bool { + return false +} + +type stubMetaFactory struct { + gvk *schema.GroupVersionKind + err error +} + +func (mf stubMetaFactory) Interpret([]byte) (*schema.GroupVersionKind, error) { + return mf.gvk, mf.err +} diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct/direct.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct/direct.go new file mode 100644 index 00000000000..cd78b1df266 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct/direct.go @@ -0,0 +1,36 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +// Package direct provides functions for marshaling and unmarshaling between arbitrary Go values and +// CBOR data, with behavior that is compatible with that of the CBOR serializer. In particular, +// types that implement cbor.Marshaler and cbor.Unmarshaler should use these functions. +package direct + +import ( + "k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes" +) + +func Marshal(src interface{}) ([]byte, error) { + return modes.Encode.Marshal(src) +} + +func Unmarshal(src []byte, dst interface{}) error { + return modes.Decode.Unmarshal(src, dst) +} + +func Diagnose(src []byte) (string, error) { + return modes.Diagnostic.Diagnose(src) +} diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/decode.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/decode.go new file mode 100644 index 00000000000..07c7474470c --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/decode.go @@ -0,0 +1,103 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package modes + +import ( + "reflect" + + "github.com/fxamacker/cbor/v2" +) + +var Decode cbor.DecMode = func() cbor.DecMode { + decode, err := cbor.DecOptions{ + // Maps with duplicate keys are well-formed but invalid according to the CBOR spec + // and never acceptable. Unlike the JSON serializer, inputs containing duplicate map + // keys are rejected outright and not surfaced as a strict decoding error. + DupMapKey: cbor.DupMapKeyEnforcedAPF, + + // For JSON parity, decoding an RFC3339 string into time.Time needs to be accepted + // with or without tagging. If a tag number is present, it must be valid. + TimeTag: cbor.DecTagOptional, + + // Observed depth up to 16 in fuzzed batch/v1 CronJobList. JSON implementation limit + // is 10000. + MaxNestedLevels: 64, + + MaxArrayElements: 1024, + MaxMapPairs: 1024, + + // Indefinite-length sequences aren't produced by this serializer, but other + // implementations can. + IndefLength: cbor.IndefLengthAllowed, + + // Accept inputs that contain CBOR tags. + TagsMd: cbor.TagsAllowed, + + // Decode type 0 (unsigned integer) as int64. + // TODO: IntDecConvertSignedOrFail errors on overflow, JSON will try to fall back to float64. + IntDec: cbor.IntDecConvertSignedOrFail, + + // Disable producing map[cbor.ByteString]interface{}, which is not acceptable for + // decodes into interface{}. + MapKeyByteString: cbor.MapKeyByteStringForbidden, + + // Error on map keys that don't map to a field in the destination struct. + ExtraReturnErrors: cbor.ExtraDecErrorUnknownField, + + // Decode maps into concrete type map[string]interface{} when the destination is an + // interface{}. + DefaultMapType: reflect.TypeOf(map[string]interface{}(nil)), + + // A CBOR text string whose content is not a valid UTF-8 sequence is well-formed but + // invalid according to the CBOR spec. Reject invalid inputs. Encoders are + // responsible for ensuring that all text strings they produce contain valid UTF-8 + // sequences and may use the byte string major type to encode strings that have not + // been validated. + UTF8: cbor.UTF8RejectInvalid, + + // Never make a case-insensitive match between a map key and a struct field. + FieldNameMatching: cbor.FieldNameMatchingCaseSensitive, + + // Produce string concrete values when decoding a CBOR byte string into interface{}. + DefaultByteStringType: reflect.TypeOf(""), + + // Allow CBOR byte strings to be decoded into string destination values. + ByteStringToString: cbor.ByteStringToStringAllowed, + + // Allow CBOR byte strings to match struct fields when appearing as a map key. + FieldNameByteString: cbor.FieldNameByteStringAllowed, + + // When decoding an unrecognized tag to interface{}, return the decoded tag content + // instead of the default, a cbor.Tag representing a (number, content) pair. + UnrecognizedTagToAny: cbor.UnrecognizedTagContentToAny, + }.DecMode() + if err != nil { + panic(err) + } + return decode +}() + +// DecodeLax is derived from Decode, but does not complain about unknown fields in the input. +var DecodeLax cbor.DecMode = func() cbor.DecMode { + opts := Decode.DecOptions() + opts.ExtraReturnErrors &^= cbor.ExtraDecErrorUnknownField // clear bit + dm, err := opts.DecMode() + if err != nil { + panic(err) + } + return dm +}() diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/diagnostic.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/diagnostic.go new file mode 100644 index 00000000000..61f3f145f55 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/diagnostic.go @@ -0,0 +1,36 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package modes + +import ( + "github.com/fxamacker/cbor/v2" +) + +var Diagnostic cbor.DiagMode = func() cbor.DiagMode { + opts := Decode.DecOptions() + diagnostic, err := cbor.DiagOptions{ + ByteStringText: true, + + MaxNestedLevels: opts.MaxNestedLevels, + MaxArrayElements: opts.MaxArrayElements, + MaxMapPairs: opts.MaxMapPairs, + }.DiagMode() + if err != nil { + panic(err) + } + return diagnostic +}() diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/encode.go b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/encode.go new file mode 100644 index 00000000000..95a9b46b716 --- /dev/null +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes/encode.go @@ -0,0 +1,96 @@ +/* +Copyright 2024 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package modes + +import ( + "github.com/fxamacker/cbor/v2" +) + +var Encode cbor.EncMode = func() cbor.EncMode { + encode, err := cbor.EncOptions{ + // Map keys need to be sorted to have deterministic output, and this is the order + // defined in RFC 8949 4.2.1 "Core Deterministic Encoding Requirements". + Sort: cbor.SortBytewiseLexical, + + // CBOR supports distinct types for IEEE-754 float16, float32, and float64. Store + // floats in the smallest width that preserves value so that equivalent float32 and + // float64 values encode to identical bytes, as they do in a JSON + // encoding. Satisfies one of the "Core Deterministic Encoding Requirements". + ShortestFloat: cbor.ShortestFloat16, + + // ShortestFloat doesn't apply to NaN or Inf values. Inf values are losslessly + // encoded to float16. RFC 8949 recommends choosing a single representation of NaN + // in applications that do not smuggle additional information inside NaN values, we + // use 0x7e00. + NaNConvert: cbor.NaNConvert7e00, + InfConvert: cbor.InfConvertFloat16, + + // Prefer encoding math/big.Int to one of the 64-bit integer types if it fits. When + // later decoded into Unstructured, the set of allowable concrete numeric types is + // limited to int64 and float64, so the distinction between big integer and integer + // can't be preserved. + BigIntConvert: cbor.BigIntConvertShortest, + + // MarshalJSON for time.Time writes RFC3339 with nanos. + Time: cbor.TimeRFC3339Nano, + + // The decoder must be able to accept RFC3339 strings with or without tag 0 (e.g. by + // the end of time.Time -> JSON -> Unstructured -> CBOR, the CBOR encoder has no + // reliable way of knowing that a particular string originated from serializing a + // time.Time), so producing tag 0 has little use. + TimeTag: cbor.EncTagNone, + + // Indefinite-length items have multiple encodings and aren't being used anyway, so + // disable to avoid an opportunity for nondeterminism. + IndefLength: cbor.IndefLengthForbidden, + + // Preserve distinction between nil and empty for slices and maps. + NilContainers: cbor.NilContainerAsNull, + + // OK to produce tags. + TagsMd: cbor.TagsAllowed, + + // Use the same definition of "empty" as encoding/json. + OmitEmpty: cbor.OmitEmptyGoValue, + + // The CBOR types text string and byte string are structurally equivalent, with the + // semantic difference that a text string whose content is an invalid UTF-8 sequence + // is itself invalid. We reject all invalid text strings at decode time and do not + // validate or sanitize all Go strings at encode time. Encoding Go strings to the + // byte string type is comparable to the existing Protobuf behavior and cheaply + // ensures that the output is valid CBOR. + String: cbor.StringToByteString, + + // Encode struct field names to the byte string type rather than the text string + // type. + FieldName: cbor.FieldNameToByteString, + }.EncMode() + if err != nil { + panic(err) + } + return encode +}() + +var EncodeNondeterministic cbor.EncMode = func() cbor.EncMode { + opts := Encode.EncOptions() + opts.Sort = cbor.SortNone + em, err := opts.EncMode() + if err != nil { + panic(err) + } + return em +}() diff --git a/vendor/modules.txt b/vendor/modules.txt index d402c586530..b864e922146 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -1313,6 +1313,7 @@ k8s.io/apimachinery/pkg/runtime k8s.io/apimachinery/pkg/runtime/schema k8s.io/apimachinery/pkg/runtime/serializer k8s.io/apimachinery/pkg/runtime/serializer/cbor +k8s.io/apimachinery/pkg/runtime/serializer/cbor/internal/modes k8s.io/apimachinery/pkg/runtime/serializer/json k8s.io/apimachinery/pkg/runtime/serializer/protobuf k8s.io/apimachinery/pkg/runtime/serializer/recognizer