mirror of
https://github.com/k3s-io/kubernetes.git
synced 2026-07-18 22:40:35 +00:00
validate/limits: update MaxLength validation to be character-based
so that it aligns with the OpenAPI maxLength validation evaluation process that performs rune counting instead of a byte-based length check Signed-off-by: Bryce Palmer <bpalmer@redhat.com>
This commit is contained in:
@@ -18,6 +18,8 @@ package validate
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"unicode/utf8"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/operation"
|
||||
"k8s.io/apimachinery/pkg/api/validate/constraints"
|
||||
@@ -31,9 +33,40 @@ func MaxLength[T ~string](_ context.Context, _ operation.Operation, fldPath *fie
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
if len(*value) > max {
|
||||
return field.ErrorList{field.TooLong(fldPath, *value, max).WithOrigin("maxLength")}
|
||||
|
||||
// if the length of the value in bytes is less
|
||||
// than the maximum size then we can confidently
|
||||
// say that this value is within the bounds
|
||||
// enforced by the maximum value regardless
|
||||
// of the actual makeup of characters in the value
|
||||
byteLength := len(*value)
|
||||
if byteLength <= max {
|
||||
return nil
|
||||
}
|
||||
|
||||
// because runes are up to 4 byte characters, if we assume all characters
|
||||
// in the input are runes, the minimum number of characters that
|
||||
// are specified is len(value)/4. If the minimum multi-byte
|
||||
// character count is greater than our enforced maximum, we
|
||||
// can confidently say that the value is invalid without having
|
||||
// to actually perform the more expensive rune counting step
|
||||
minimum := int(math.Ceil(float64(byteLength) / 4.0))
|
||||
if minimum > max || utf8.RuneCountInString(string(*value)) > max {
|
||||
return field.ErrorList{field.TooLongCharacters(fldPath, *value, max).WithOrigin("maxLength")}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MaxBytes verifies that the specified value is not longer than max bytes.
|
||||
func MaxBytes[T ~string](_ context.Context, _ operation.Operation, fldPath *field.Path, value, _ *T, max int) field.ErrorList {
|
||||
if value == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(*value) > max {
|
||||
return field.ErrorList{field.TooLong(fldPath, *value, max).WithOrigin("maxBytes")}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ func TestMaxLength(t *testing.T) {
|
||||
value: "0",
|
||||
max: 0,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("fldpath"), nil, 0).WithOrigin("maxLength"),
|
||||
field.TooLongCharacters(field.NewPath("fldpath"), "", 0).WithOrigin("maxLength"),
|
||||
},
|
||||
}, {
|
||||
name: "one character",
|
||||
@@ -54,14 +54,55 @@ func TestMaxLength(t *testing.T) {
|
||||
value: "01",
|
||||
max: 1,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("fldpath"), nil, 1).WithOrigin("maxLength"),
|
||||
field.TooLongCharacters(field.NewPath("fldpath"), "", 1).WithOrigin("maxLength"),
|
||||
},
|
||||
}, {
|
||||
value: "",
|
||||
max: -1,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("fldpath"), nil, -1).WithOrigin("maxLength"),
|
||||
field.TooLongCharacters(field.NewPath("fldpath"), "", -1).WithOrigin("maxLength"),
|
||||
},
|
||||
}, {
|
||||
name: "ascii-only characters, less characters than max (n-1)",
|
||||
value: "abcdefghi",
|
||||
max: 10,
|
||||
wantErrs: nil,
|
||||
}, {
|
||||
name: "multi-byte characters, less characters than max (n-1)",
|
||||
value: "©®©®©®©®©",
|
||||
max: 10,
|
||||
wantErrs: nil,
|
||||
}, {
|
||||
name: "ascii-only characters, more characters than max (n+1)",
|
||||
value: "abcdefghijkl",
|
||||
max: 10,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLongCharacters(field.NewPath("fldpath"), "", 10).WithOrigin("maxLength"),
|
||||
},
|
||||
}, {
|
||||
name: "multi-byte characters, more characters than max (n+1)",
|
||||
value: "©®©®©®©®©®©",
|
||||
max: 10,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLongCharacters(field.NewPath("fldpath"), "", 10).WithOrigin("maxLength"),
|
||||
},
|
||||
}, {
|
||||
name: "mixture of characters, minimum possible size of input is less than max, rune count exceed maximum",
|
||||
value: "©abc®defghi",
|
||||
max: 10,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLongCharacters(field.NewPath("fldpath"), "", 10).WithOrigin("maxLength"),
|
||||
},
|
||||
}, {
|
||||
name: "multi-byte characters, exact characters as max (n)",
|
||||
value: "©®©®©®©®©®",
|
||||
max: 10,
|
||||
wantErrs: nil,
|
||||
}, {
|
||||
name: "ascii-only characters, exact characters as max (n)",
|
||||
value: "abcdefghij",
|
||||
max: 10,
|
||||
wantErrs: nil,
|
||||
}}
|
||||
|
||||
matcher := field.ErrorMatcher{}.ByOrigin().ByDetailSubstring().ByField().ByType()
|
||||
@@ -213,3 +254,87 @@ func doTestMinimum[T constraints.Integer](t *testing.T, cases []minimumTestCase[
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxBytes(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
value string
|
||||
max int
|
||||
wantErrs field.ErrorList // regex
|
||||
}{{
|
||||
name: "empty string",
|
||||
value: "",
|
||||
max: 0,
|
||||
wantErrs: nil,
|
||||
}, {
|
||||
name: "zero length",
|
||||
value: "0",
|
||||
max: 0,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("fldpath"), "", 0).WithOrigin("maxBytes"),
|
||||
},
|
||||
}, {
|
||||
name: "one character",
|
||||
value: "0",
|
||||
max: 1,
|
||||
wantErrs: nil,
|
||||
}, {
|
||||
name: "two characters",
|
||||
value: "01",
|
||||
max: 1,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("fldpath"), "", 1).WithOrigin("maxBytes"),
|
||||
},
|
||||
}, {
|
||||
value: "",
|
||||
max: -1,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("fldpath"), "", -1).WithOrigin("maxBytes"),
|
||||
},
|
||||
}, {
|
||||
name: "ascii-only characters, less bytes than max",
|
||||
value: "abcdefghi",
|
||||
max: 10,
|
||||
wantErrs: nil,
|
||||
}, {
|
||||
name: "multi-byte characters, less bytes than max",
|
||||
value: "©®©®",
|
||||
max: 10,
|
||||
wantErrs: nil,
|
||||
}, {
|
||||
name: "ascii-only characters, more bytes than max",
|
||||
value: "abcdefghijkl",
|
||||
max: 10,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("fldpath"), "", 10).WithOrigin("maxBytes"),
|
||||
},
|
||||
}, {
|
||||
name: "multi-byte characters, more bytes than max",
|
||||
value: "©®©®©©",
|
||||
max: 10,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("fldpath"), "", 10).WithOrigin("maxBytes"),
|
||||
},
|
||||
}, {
|
||||
name: "mixture of characters, less bytes than max",
|
||||
value: "©abc®®",
|
||||
max: 10,
|
||||
wantErrs: nil,
|
||||
}, {
|
||||
name: "mixture of characters, more bytes than max",
|
||||
value: "©abc®®abc",
|
||||
max: 10,
|
||||
wantErrs: field.ErrorList{
|
||||
field.TooLong(field.NewPath("fldpath"), "", 10).WithOrigin("maxBytes"),
|
||||
},
|
||||
}}
|
||||
|
||||
matcher := field.ErrorMatcher{}.ByOrigin().ByDetailSubstring().ByField().ByType()
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
v := tc.value
|
||||
gotErrs := MaxBytes(context.Background(), operation.Operation{}, field.NewPath("fldpath"), &v, nil, tc.max)
|
||||
matcher.Test(t, tc.wantErrs, gotErrs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user