diff --git a/pkg/controller/hash.go b/pkg/controller/hash.go index f66790e..875f04b 100644 --- a/pkg/controller/hash.go +++ b/pkg/controller/hash.go @@ -13,12 +13,13 @@ import ( ) var ( - ErrInvalidNames = errors.New("list of names must not be empty and contain at least one non-empty string") + ErrInvalidNames = errors.New("list of names must not be empty and contain at least one non-empty string") + ErrMaxLenTooSmall = errors.New("maxLen must be greater than 10") ) -// Version8UUID creates a new UUID (version 8) from a byte slice. Returns an error if the slice does not have a length of 16. The bytes are copied from the slice. +// version8UUID creates a new UUID (version 8) from a byte slice. Returns an error if the slice does not have a length of 16. The bytes are copied from the slice. // The bits 48-51 and 64-65 are modified to make the output recognizable as a version 8 UUID, so only 122 out of 128 bits from the input data will be kept. -func Version8UUID(data []byte) (uuid.UUID, error) { +func version8UUID(data []byte) (uuid.UUID, error) { if len(data) != 16 { return uuid.Nil, fmt.Errorf("invalid data (got %d bytes)", len(data)) } @@ -40,6 +41,7 @@ func Version8UUID(data []byte) (uuid.UUID, error) { // K8sNameUUID takes any number of string arguments and computes a hash out of it, which is then formatted as a version 8 UUID. // The arguments are joined with '/' before being hashed. // Returns an error if the list of ids is empty or contains only empty strings. +// Deprecated: Use NameHashSHAKE128Base32 instead. func K8sNameUUID(names ...string) (string, error) { if err := validateIDs(names); err != nil { return "", err @@ -47,7 +49,7 @@ func K8sNameUUID(names ...string) (string, error) { name := strings.Join(names, "/") hash := sha3.SumSHAKE128([]byte(name), 16) - u, err := Version8UUID(hash) + u, err := version8UUID(hash) return u.String(), err } @@ -64,6 +66,7 @@ func validateIDs(names []string) error { // K8sNameUUIDUnsafe works like K8sNameUUID, but panics instead of returning an error. // This should only be used in places where the input is guaranteed to be valid. +// Deprecated: Use NameHashSHAKE128Base32 instead. func K8sNameUUIDUnsafe(names ...string) string { uuid, err := K8sNameUUID(names...) if err != nil { @@ -74,6 +77,7 @@ func K8sNameUUIDUnsafe(names ...string) string { // K8sObjectUUID takes a client object and computes a hash out of the namespace and name, which is then formatted as a version 8 UUID. // An empty namespace will be replaced by "default". +// Deprecated: Use ObjectHashSHAKE128Base32 instead. func K8sObjectUUID(obj client.Object) (string, error) { name, namespace := obj.GetName(), obj.GetNamespace() if namespace == "" { @@ -84,6 +88,7 @@ func K8sObjectUUID(obj client.Object) (string, error) { // K8sObjectUUIDUnsafe works like K8sObjectUUID, but panics instead of returning an error. // This should only be used in places where the input is guaranteed to be valid. +// Deprecated: Use ObjectHashSHAKE128Base32 instead. func K8sObjectUUIDUnsafe(obj client.Object) string { uuid, err := K8sObjectUUID(obj) if err != nil { @@ -113,3 +118,21 @@ func ObjectHashSHAKE128Base32(obj client.Object) string { } return NameHashSHAKE128Base32(namespace, name) } + +// ShortenToXCharacters shortens the input string if it exceeds maxLen. +// It uses NameHashSHAKE128Base32 to generate a hash which will replace the last few characters. +func ShortenToXCharacters(input string, maxLen int) (string, error) { + if len(input) <= maxLen { + return input, nil + } + + hash := NameHashSHAKE128Base32(input) + suffix := fmt.Sprintf("--%s", hash) + trimLength := maxLen - len(suffix) + + if trimLength <= 0 { + return "", ErrMaxLenTooSmall + } + + return input[:trimLength] + suffix, nil +} diff --git a/pkg/controller/hash_test.go b/pkg/controller/hash_test.go index 34f13f7..e48b9e0 100644 --- a/pkg/controller/hash_test.go +++ b/pkg/controller/hash_test.go @@ -44,7 +44,7 @@ func Test_Version8UUID(t *testing.T) { } for _, tC := range testCases { t.Run(tC.desc, func(t *testing.T) { - u, err := Version8UUID(tC.data) + u, err := version8UUID(tC.data) if tC.expectedErr == nil { assert.NoError(t, err) assert.Equal(t, uuid.Version(8), u.Version(), "unexpected version") @@ -189,3 +189,49 @@ func Test_ObjectHashSHAKE128Base32(t *testing.T) { }) } } + +func Test_ShortenToXCharacters(t *testing.T) { + testCases := []struct { + desc string + input string + maxLen int + expected string + expectedErr error + }{ + { + desc: "short string", + input: "short", + expected: "short", + maxLen: 100, + }, + { + desc: "short string to trim", + input: "short1234567", + expected: "s--j5gore3p", + maxLen: 11, + }, + { + desc: "maxLen too small", + input: "shorter1234", + maxLen: 10, + expectedErr: ErrMaxLenTooSmall, + }, + { + desc: "long string", + input: "this-is-a-very-a-very-a-very-long-string-that-is-over-63-characters", + expected: "this-is-a-very-a-very-a-very-long-string-that-is-over--6reoyp5o", + maxLen: 63, + }, + } + for _, tC := range testCases { + t.Run(tC.desc, func(t *testing.T) { + actual, err := ShortenToXCharacters(tC.input, tC.maxLen) + if tC.expectedErr == nil { + assert.Equal(t, tC.expected, actual) + assert.LessOrEqual(t, len(actual), tC.maxLen) + } else { + assert.Equal(t, tC.expectedErr, err) + } + }) + } +} diff --git a/pkg/controller/utils.go b/pkg/controller/utils.go index 44423a5..db5bd29 100644 --- a/pkg/controller/utils.go +++ b/pkg/controller/utils.go @@ -15,6 +15,7 @@ const ( // K8sNameHash takes any number of string arguments and computes a hash out of it, which is then base32-encoded to be a valid DNS1123Subdomain k8s resource name // The arguments are joined with '/' before being hashed. +// Deprecated: Use NameHashSHAKE128Base32 instead. func K8sNameHash(ids ...string) string { name := strings.Join(ids, "/") // since we are not worried about length-extension attacks (in fact we are not even using hashing for