diff --git a/staging/src/k8s.io/apimachinery/pkg/util/intstr/intstr.go b/staging/src/k8s.io/apimachinery/pkg/util/intstr/intstr.go index f358c794d10..5fd2e16c842 100644 --- a/staging/src/k8s.io/apimachinery/pkg/util/intstr/intstr.go +++ b/staging/src/k8s.io/apimachinery/pkg/util/intstr/intstr.go @@ -25,6 +25,7 @@ import ( "strconv" "strings" + cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct" "k8s.io/klog/v2" ) @@ -92,6 +93,20 @@ func (intstr *IntOrString) UnmarshalJSON(value []byte) error { return json.Unmarshal(value, &intstr.IntVal) } +func (intstr *IntOrString) UnmarshalCBOR(value []byte) error { + if err := cbor.Unmarshal(value, &intstr.StrVal); err == nil { + intstr.Type = String + return nil + } + + if err := cbor.Unmarshal(value, &intstr.IntVal); err != nil { + return err + } + + intstr.Type = Int + return nil +} + // String returns the string value, or the Itoa of the int value. func (intstr *IntOrString) String() string { if intstr == nil { @@ -126,6 +141,17 @@ func (intstr IntOrString) MarshalJSON() ([]byte, error) { } } +func (intstr IntOrString) MarshalCBOR() ([]byte, error) { + switch intstr.Type { + case Int: + return cbor.Marshal(intstr.IntVal) + case String: + return cbor.Marshal(intstr.StrVal) + default: + return nil, fmt.Errorf("impossible IntOrString.Type") + } +} + // OpenAPISchemaType is used by the kube-openapi generator when constructing // the OpenAPI spec of this type. // diff --git a/staging/src/k8s.io/apimachinery/pkg/util/intstr/intstr_test.go b/staging/src/k8s.io/apimachinery/pkg/util/intstr/intstr_test.go index 197e58d2d41..ecf47ce0da8 100644 --- a/staging/src/k8s.io/apimachinery/pkg/util/intstr/intstr_test.go +++ b/staging/src/k8s.io/apimachinery/pkg/util/intstr/intstr_test.go @@ -18,10 +18,16 @@ package intstr import ( "encoding/json" + "fmt" + "math" "reflect" "testing" + cbor "k8s.io/apimachinery/pkg/runtime/serializer/cbor/direct" "sigs.k8s.io/yaml" + + "github.com/google/go-cmp/cmp" + fuzz "github.com/google/gofuzz" ) func TestFromInt(t *testing.T) { @@ -324,3 +330,239 @@ func TestParse(t *testing.T) { } } } + +func TestMarshalCBOR(t *testing.T) { + for _, tc := range []struct { + in IntOrString + want []byte + assertOnError func(*testing.T, error) + }{ + { + in: IntOrString{Type: 42}, + assertOnError: func(t *testing.T, err error) { + if err == nil { + t.Fatal("expected non-nil error") + } + const want = "impossible IntOrString.Type" + if got := err.Error(); got != want { + t.Fatalf("want error message %q, got %q", want, got) + } + }, + }, + { + in: FromString(""), + want: []byte{0x40}, + }, + { + in: FromString("abc"), + want: []byte{0x43, 'a', 'b', 'c'}, + }, + { + in: FromInt32(0), // min positive integer representable in one byte + want: []byte{0x00}, + }, + { + in: FromInt32(23), // max positive integer representable in one byte + want: []byte{0x17}, + }, + { + in: FromInt32(24), // min positive integer representable in two bytes + want: []byte{0x18, 0x18}, + }, + { + in: FromInt32(math.MaxUint8), // max positive integer representable in two bytes + want: []byte{0x18, 0xff}, + }, + { + in: FromInt32(math.MaxUint8 + 1), // min positive integer representable in three bytes + want: []byte{0x19, 0x01, 0x00}, + }, + { + in: FromInt32(math.MaxUint16), // max positive integer representable in three bytes + want: []byte{0x19, 0xff, 0xff}, + }, + { + in: FromInt32(math.MaxUint16 + 1), // min positive integer representable in five bytes + want: []byte{0x1a, 0x00, 0x01, 0x00, 0x00}, + }, + { + in: FromInt32(math.MaxInt32), // max positive integer representable by Go int32 + want: []byte{0x1a, 0x7f, 0xff, 0xff, 0xff}, + }, + { + in: FromInt32(-1), // max negative integer representable in one byte + want: []byte{0x20}, + }, + { + in: FromInt32(-24), // min negative integer representable in one byte + want: []byte{0x37}, + }, + { + in: FromInt32(-1 - 24), // max negative integer representable in two bytes + want: []byte{0x38, 0x18}, + }, + { + in: FromInt32(-1 - math.MaxUint8), // min negative integer representable in two bytes + want: []byte{0x38, 0xff}, + }, + { + in: FromInt32(-2 - math.MaxUint8), // max negative integer representable in three bytes + want: []byte{0x39, 0x01, 0x00}, + }, + { + in: FromInt32(-1 - math.MaxUint16), // min negative integer representable in three bytes + want: []byte{0x39, 0xff, 0xff}, + }, + { + in: FromInt32(-2 - math.MaxUint16), // max negative integer representable in five bytes + want: []byte{0x3a, 0x00, 0x01, 0x00, 0x00}, + }, + { + in: FromInt32(math.MinInt32), // min negative integer representable by Go int32 + want: []byte{0x3a, 0x7f, 0xff, 0xff, 0xff}, + }, + } { + t.Run(fmt.Sprintf("{Type:%d,IntVal:%d,StrVal:%q}", tc.in.Type, tc.in.IntVal, tc.in.StrVal), func(t *testing.T) { + got, err := tc.in.MarshalCBOR() + if tc.assertOnError != nil { + tc.assertOnError(t, err) + } else if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if diff := cmp.Diff(got, tc.want); diff != "" { + t.Errorf("unexpected difference between expected and actual output:\n%s", diff) + } + }) + } +} + +func TestUnmarshalCBOR(t *testing.T) { + for _, tc := range []struct { + in []byte + want IntOrString + assertOnError func(*testing.T, error) + }{ + { + in: []byte{0xa0}, // {} + assertOnError: func(t *testing.T, err error) { + if err == nil { + t.Fatal("expected non-nil error") + } + const want = "cbor: cannot unmarshal map into Go value of type int32" + if got := err.Error(); got != want { + t.Fatalf("want error message %q, got %q", want, got) + } + + }, + }, + { + in: []byte{0x40}, + want: FromString(""), + }, + { + in: []byte{0x43, 'a', 'b', 'c'}, + want: FromString("abc"), + }, + { + in: []byte{0x00}, + want: FromInt32(0), // min positive integer representable in one byte + }, + { + in: []byte{0x17}, + want: FromInt32(23), // max positive integer representable in one byte + }, + { + in: []byte{0x18, 0x18}, + want: FromInt32(24), // min positive integer representable in two bytes + }, + { + in: []byte{0x18, 0xff}, + want: FromInt32(math.MaxUint8), // max positive integer representable in two bytes + }, + { + in: []byte{0x19, 0x01, 0x00}, + want: FromInt32(math.MaxUint8 + 1), // min positive integer representable in three bytes + }, + { + in: []byte{0x19, 0xff, 0xff}, + want: FromInt32(math.MaxUint16), // max positive integer representable in three bytes + }, + { + in: []byte{0x1a, 0x00, 0x01, 0x00, 0x00}, + want: FromInt32(math.MaxUint16 + 1), // min positive integer representable in five bytes + }, + { + in: []byte{0x1a, 0x7f, 0xff, 0xff, 0xff}, + want: FromInt32(math.MaxInt32), // max positive integer representable by Go int32 + }, + { + in: []byte{0x20}, + want: FromInt32(-1), // max negative integer representable in one byte + }, + { + in: []byte{0x37}, + want: FromInt32(-24), // min negative integer representable in one byte + }, + { + in: []byte{0x38, 0x18}, + want: FromInt32(-1 - 24), // max negative integer representable in two bytes + }, + { + in: []byte{0x38, 0xff}, + want: FromInt32(-1 - math.MaxUint8), // min negative integer representable in two bytes + }, + { + in: []byte{0x39, 0x01, 0x00}, + want: FromInt32(-2 - math.MaxUint8), // max negative integer representable in three bytes + }, + { + in: []byte{0x39, 0xff, 0xff}, + want: FromInt32(-1 - math.MaxUint16), // min negative integer representable in three bytes + }, + { + in: []byte{0x3a, 0x00, 0x01, 0x00, 0x00}, + want: FromInt32(-2 - math.MaxUint16), // max negative integer representable in five bytes + }, + { + in: []byte{0x3a, 0x7f, 0xff, 0xff, 0xff}, + want: FromInt32(math.MinInt32), // min negative integer representable by Go int32 + }, + } { + t.Run(fmt.Sprintf("{Type:%d,IntVal:%d,StrVal:%q}", tc.want.Type, tc.want.IntVal, tc.want.StrVal), func(t *testing.T) { + var got IntOrString + err := got.UnmarshalCBOR(tc.in) + if tc.assertOnError != nil { + tc.assertOnError(t, err) + } else if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if diff := cmp.Diff(got, tc.want); diff != "" { + t.Errorf("unexpected difference between expected and actual output:\n%s", diff) + } + }) + } +} + +func TestIntOrStringRoundtripCBOR(t *testing.T) { + fuzzer := fuzz.New() + for i := 0; i < 500; i++ { + var initial, final IntOrString + fuzzer.Fuzz(&initial) + b, err := cbor.Marshal(initial) + if err != nil { + t.Errorf("error encoding %v: %v", initial, err) + continue + } + err = cbor.Unmarshal(b, &final) + if err != nil { + t.Errorf("%v: error decoding %v: %v", initial, string(b), err) + } + if diff := cmp.Diff(initial, final); diff != "" { + diag, err := cbor.Diagnose(b) + if err != nil { + t.Logf("failed to produce diagnostic encoding of 0x%x: %v", b, err) + } + t.Errorf("unexpected diff:\n%s\ncbor: %s", diff, diag) + } + } +}