-
Notifications
You must be signed in to change notification settings - Fork 5
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
6 changed files
with
320 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,4 +23,5 @@ test-report.json | |
junit*.xml | ||
*cov | ||
|
||
*.test | ||
*.test | ||
kubeutils/testdata/fuzz/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
changelog: | ||
- type: NON_USER_FACING | ||
description: Fix and make santize faster |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
package kubeutils | ||
|
||
import ( | ||
"bytes" | ||
"crypto/md5" | ||
"encoding/hex" | ||
"hash" | ||
"sync" | ||
) | ||
|
||
// This is an extrememly important number for these shortened names. | ||
// It signifies the spot of separation from the original name and the hash. | ||
// It is used for many string building and parsing operations below. | ||
const magicNumber = 31 | ||
const totalSize = magicNumber*2 + 1 | ||
const encodedMd5 = 2 * md5.Size | ||
|
||
const separator = '-' | ||
|
||
// We can short-circuit the comparison if the first 31 characters are not equal. | ||
// Otherweise we need to compare the shortened version of the strings. | ||
func ShortenedEquals(shortened, standard string) bool { | ||
|
||
// If the standard string is less than 63 characters, we can just compare the strings. | ||
if len(standard) <= totalSize { | ||
return shortened == standard | ||
} | ||
|
||
// If the shortened string is less than or equal to 32 characters, we can just compare the strings. | ||
// Also if it's less than 32 the below checks may crash. | ||
if len(shortened) <= magicNumber+1 { | ||
return shortened == standard | ||
} | ||
|
||
// Check the first 31 characters, if they're not equal we can exit early. | ||
if shortened[:magicNumber] != standard[:magicNumber] { | ||
return false | ||
} | ||
|
||
// If 32nd character of the shortened string is not a '-' or the 32nd character of the standard string is not a '-' | ||
// we can exit early. | ||
// In theory this shouldn't be necessary, but this label can technically be modified by the user, | ||
// so it's safer to double check. | ||
if shortened[magicNumber] != separator { | ||
return false | ||
} | ||
|
||
// Check the last 32 characters of the shortened string against the hash of the standard string. | ||
hashed := hashName(standard) | ||
return shortened[magicNumber+1:] == string(hashed[:magicNumber]) | ||
} | ||
|
||
// shortenName is extrememly inefficient with it's allocation of slices for hashing. | ||
// We can re-use the arrays to avoid this allocation. However, this code may be called | ||
// from multiple go-routines simultaneously so we must house these objects in sync.Pools | ||
|
||
// Pool of MD5 hashers to avoid allocation. | ||
var md5HasherPool = sync.Pool{ | ||
New: func() interface{} { | ||
return md5.New() | ||
}, | ||
} | ||
|
||
// Pool of string builders to avoid allocation. | ||
var byteBufferPool = sync.Pool{ | ||
New: func() interface{} { | ||
b := &bytes.Buffer{} | ||
b.Grow(totalSize) | ||
return b | ||
}, | ||
} | ||
|
||
// hashName returns a hash of the input string in base 16 format | ||
// This function is optimized for speed and memory usage. | ||
// It should aboid nearly all allocations by re-using the same buffers whenever possible. | ||
func hashName(name string) [encodedMd5]byte { | ||
hasher := md5HasherPool.Get().(hash.Hash) | ||
hasher.Reset() | ||
hasher.Write([]byte(name)) | ||
hashArray := [md5.Size]byte{} | ||
hash := hasher.Sum(hashArray[:0]) | ||
// Cannot use hex.EncodedLen() here because it's a func, but it just returns 2 * len(src) | ||
hashBufferArray := [encodedMd5]byte{} | ||
hex.Encode(hashBufferArray[:], hash) | ||
md5HasherPool.Put(hasher) | ||
return hashBufferArray | ||
} | ||
|
||
// shortenName returns a shortened version of the input string. | ||
// It is based on the `kubeutils.SanitizeNameV2` function, but it | ||
// just does the shortening part. | ||
func ShortenName(name string) string { | ||
if len(name) > totalSize { | ||
hash := hashName(name) | ||
builder := byteBufferPool.Get().(*bytes.Buffer) | ||
builder.Reset() | ||
builder.Grow(totalSize) | ||
builder.WriteString(name[:magicNumber]) | ||
builder.WriteRune(separator) | ||
builder.Write(hash[:magicNumber]) | ||
name = builder.String() | ||
byteBufferPool.Put(builder) | ||
} | ||
return name | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,137 @@ | ||
package kubeutils | ||
|
||
import ( | ||
"crypto/md5" | ||
"fmt" | ||
"testing" | ||
) | ||
|
||
// This function is a copy of the old version of this function. | ||
// It is used to ensure parity with the old implementation. | ||
func shortenName(name string) string { | ||
if len(name) > 63 { | ||
hash := md5.Sum([]byte(name)) | ||
name = fmt.Sprintf("%s-%x", name[:31], hash) | ||
name = name[:63] | ||
} | ||
return name | ||
} | ||
|
||
func BenchmarkShortenEqual(b *testing.B) { | ||
b.Run("shorten name old", func(b *testing.B) { | ||
for i := 0; i < b.N; i++ { | ||
shortenName("jfdklanfkljasfhjhldacaslkhdfkjshfkjsadhfkjasdhgjadhgkdahfjkdahjfdsagdfhjdsagfhasjdfsdfasfsafsdf") | ||
} | ||
}) | ||
|
||
b.Run("shortened equals--worst case", func(b *testing.B) { | ||
shortened := "jfdklanfkljasfhjhldacaslkhdfkjs-f1e0028d0fbfe9afbd1a8bb9b53848d" | ||
standard := "jfdklanfkljasfhjhldacaslkhdfkjshfkjsadhfkjasdhgjadhgkdahfjkdahjfdsagdfhjdsagfhasjdfsdfasfsafsdf" | ||
for i := 0; i < b.N; i++ { | ||
ShortenedEquals(shortened, standard) | ||
} | ||
}) | ||
|
||
b.Run("shortened equals--different prefix", func(b *testing.B) { | ||
shortened := "jfdklanfkljasfhjhlxacaslkhdfkjs-f1e0028d0fbfe9afbd1a8bb9b53848d" | ||
standard := "jfdklanfkljasfhjhldacaslkhdfkjshfkjsadhfkjasdhgjadhgkdahfjkdahjfdsagdfhjdsagfhasjdfsdfasfsafsdf" | ||
for i := 0; i < b.N; i++ { | ||
ShortenedEquals(shortened, standard) | ||
} | ||
}) | ||
|
||
b.Run("shortened equals--less than 63 characters", func(b *testing.B) { | ||
shortened := "hello" | ||
standard := "hello" | ||
for i := 0; i < b.N; i++ { | ||
ShortenedEquals(shortened, standard) | ||
} | ||
}) | ||
} | ||
|
||
func FuzzShortNameParity(f *testing.F) { | ||
// Random string < 63 | ||
f.Add("hello") | ||
// Random string > 63 | ||
f.Add("jfdklanfkljasfhjhldacaslkhdfkjshfkjsadhfkjasdhgjadhgkdahfjkdahjfdsagdfhjdsagfhasjdfsdfasfsafsdf") | ||
f.Fuzz(func(t *testing.T, a string) { | ||
oldName := shortenName(a) | ||
newName := ShortenName(a) | ||
if oldName != newName { | ||
t.Fatalf("shortenName(%s) = %s, ShortenName(%s) = %s", a, oldName, a, newName) | ||
} | ||
|
||
equal := ShortenedEquals(newName, a) | ||
if !equal { | ||
t.Fatalf("ShortenedEquals(%s, %s) = %t", newName, a, equal) | ||
} | ||
}) | ||
} | ||
|
||
func TestShortenName(t *testing.T) { | ||
t.Run("shorten name < 63", func(t *testing.T) { | ||
name := "hello" | ||
shortened := ShortenName(name) | ||
if shortened != name { | ||
t.Fatalf("ShortenName(%s) = %s", name, shortened) | ||
} | ||
}) | ||
|
||
t.Run("shorten name > 63", func(t *testing.T) { | ||
name := "jfdklanfkljasfhjhldacaslkhdfkjshfkjsadhfkjasdhgjadhgkdahfjkdahjfdsagdfhjdsagfhasjdfsdfasfsafsdf" | ||
shortened := ShortenName(name) | ||
if len(shortened) != 63 { | ||
t.Fatalf("ShortenName(%s) = %s", name, shortened) | ||
} | ||
|
||
if shortened != "jfdklanfkljasfhjhldacaslkhdfkjs-f1e0028d0fbfe9afbd1a8bb9b53848d" { | ||
t.Fatalf("ShortenName(%s) = %s", name, shortened) | ||
} | ||
}) | ||
} | ||
|
||
func TestShortenedEquals(t *testing.T) { | ||
|
||
testCases := []struct { | ||
name string | ||
shortened string | ||
equal bool | ||
}{ | ||
{ | ||
name: "hello", | ||
shortened: "hello", | ||
equal: true, | ||
}, | ||
{ | ||
name: "jfdklanfkljasfhjhldacaslkhdfkjshfkjsadhfkjasdhgjadhgkdahfjkdahjfdsagdfhjdsagfhasjdfsdfasfsafsdf", | ||
shortened: "jfdklanfkljasfhjhldacaslkhdfkjs-f1e0028d0fbfe9afbd1a8bb9b53848d", | ||
equal: true, | ||
}, | ||
{ | ||
name: "jfdklanfkljasfhjhldacaslkhdfkjshfkjsadhfkjasdhgjadhgkdahfjkdahjfdsagdfhjdsagfhasjdfsdfasfsafsdf", | ||
shortened: "jfdklanfkljasfhjhldacaslkhdfkjs-f1e0028d0fbfe9afbd1a8bb9b53848", | ||
equal: false, | ||
}, | ||
{ | ||
name: "jfdklanfkljasfhjhldacaslkhdfkjshfkjsadhfkjasdhgjadhgkdahfjkdahjfdsagdfhjdsagfhasjdfsdfasfsafsdf", | ||
shortened: "jfdklanfkljasfhjhldacaslkhdfkjsf1e0028d0fbfe9afbd1a8bb9b53848ds", | ||
equal: false, | ||
}, | ||
{ | ||
name: "jfdklanfkljasfhjhldacaslkhdfkjshfkjsadhfkjasdhgjadhgkdahfjkdahjfdsagdfhjdsagfhasjdfsdfasfsafsdf", | ||
shortened: "jfdklanfkjasfhjhldacaslkhdfkjs-f1e0028d0fbfe9afbd1a8bb9b53848ds", | ||
equal: false, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
equal := ShortenedEquals(tc.shortened, tc.name) | ||
if equal != tc.equal { | ||
t.Fatalf("ShortenedEquals(%s, %s) = %t", tc.shortened, tc.name, equal) | ||
} | ||
}) | ||
|
||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters