diff --git a/common.go b/common.go index 30a223d..f2e65e5 100644 --- a/common.go +++ b/common.go @@ -9,7 +9,6 @@ import ( "crypto/rand" "encoding/binary" "encoding/hex" - "math" "net" "os" "reflect" @@ -17,7 +16,6 @@ import ( "strconv" "sync" "sync/atomic" - "unicode" "github.com/google/uuid" ) @@ -32,10 +30,11 @@ const ( // All rights reserved. var ( - uuidSeed [24]byte - uuidCounter uint64 - uuidSetup sync.Once - unitsSlice = []byte("kmgtp") + uuidSeed [24]byte + uuidCounter uint64 + uuidSetup sync.Once + unitsSlice = []byte("kmgtp") + sizeMultipliers = [...]float64{1e3, 1e6, 1e9, 1e12, 1e15} ) // UUID generates an universally unique identifier (UUID) @@ -125,10 +124,11 @@ func ConvertToBytes(humanReadableString string) int { // loop the string for i := strLen - 1; i >= 0; i-- { // check if the char is a number - if unicode.IsDigit(rune(humanReadableString[i])) { + c := humanReadableString[i] + if c >= '0' && c <= '9' { lastNumberPos = i break - } else if humanReadableString[i] != ' ' { + } else if c != ' ' { unitPrefixPos = i } } @@ -144,7 +144,7 @@ func ConvertToBytes(humanReadableString string) int { // convert multiplier char to lowercase and check if exists in units slice index := bytes.IndexByte(unitsSlice, toLowerTable[humanReadableString[unitPrefixPos]]) if index != -1 { - size *= math.Pow(1000, float64(index+1)) + size *= sizeMultipliers[index] } } diff --git a/common_test.go b/common_test.go index 65cbb22..a673faf 100644 --- a/common_test.go +++ b/common_test.go @@ -75,13 +75,26 @@ func Test_UUIDv4_Concurrency(t *testing.T) { func Test_ConvertToBytes(t *testing.T) { t.Parallel() + // initial assertions require.Equal(t, 0, ConvertToBytes("")) require.Equal(t, 42, ConvertToBytes("42")) + + // Test empty string + require.Equal(t, 0, ConvertToBytes("")) + + // Test basic numbers (digit detection optimization) + require.Equal(t, 42, ConvertToBytes("42")) + require.Equal(t, 0, ConvertToBytes("0")) + require.Equal(t, 1, ConvertToBytes("1")) + require.Equal(t, 999, ConvertToBytes("999")) + + // Test with 'b' and 'B' suffixes require.Equal(t, 42, ConvertToBytes("42b")) require.Equal(t, 42, ConvertToBytes("42B")) require.Equal(t, 42, ConvertToBytes("42 b")) require.Equal(t, 42, ConvertToBytes("42 B")) + // Test sizeMultipliers array usage (k/K - 1e3) require.Equal(t, 42*1000, ConvertToBytes("42k")) require.Equal(t, 42*1000, ConvertToBytes("42K")) require.Equal(t, 42*1000, ConvertToBytes("42kb")) @@ -89,12 +102,71 @@ func Test_ConvertToBytes(t *testing.T) { require.Equal(t, 42*1000, ConvertToBytes("42 kb")) require.Equal(t, 42*1000, ConvertToBytes("42 KB")) + // Test sizeMultipliers array usage (m/M - 1e6) require.Equal(t, 42*1000000, ConvertToBytes("42M")) + require.Equal(t, 42*1000000, ConvertToBytes("42m")) + require.Equal(t, 42*1000000, ConvertToBytes("42MB")) + require.Equal(t, 42*1000000, ConvertToBytes("42mb")) require.Equal(t, int(42.5*1000000), ConvertToBytes("42.5MB")) - require.Equal(t, 42*1000000000, ConvertToBytes("42G")) + // Test sizeMultipliers array usage (g/G - 1e9) + require.Equal(t, 42*1000000000, ConvertToBytes("42G")) + require.Equal(t, 42*1000000000, ConvertToBytes("42g")) + require.Equal(t, 42*1000000000, ConvertToBytes("42GB")) + require.Equal(t, 42*1000000000, ConvertToBytes("42gb")) + + // Test sizeMultipliers array usage (t/T - 1e12) + require.Equal(t, 42*1000000000000, ConvertToBytes("42T")) + require.Equal(t, 42*1000000000000, ConvertToBytes("42t")) + require.Equal(t, 42*1000000000000, ConvertToBytes("42TB")) + require.Equal(t, 42*1000000000000, ConvertToBytes("42tb")) + + // Test sizeMultipliers array usage (p/P - 1e15) + require.Equal(t, 42*1000000000000000, ConvertToBytes("42P")) + require.Equal(t, 42*1000000000000000, ConvertToBytes("42p")) + require.Equal(t, 42*1000000000000000, ConvertToBytes("42PB")) + require.Equal(t, 42*1000000000000000, ConvertToBytes("42pb")) + + // Test edge cases and error conditions require.Equal(t, 0, ConvertToBytes("string")) require.Equal(t, 0, ConvertToBytes("MB")) + require.Equal(t, 0, ConvertToBytes("invalidunit")) + require.Equal(t, 42, ConvertToBytes("42X")) // invalid unit + require.Equal(t, 0, ConvertToBytes("42.5.5MB")) // invalid format + + // Test decimal numbers with various units + require.Equal(t, int(1.5*1000), ConvertToBytes("1.5k")) + require.Equal(t, int(2.25*1000000), ConvertToBytes("2.25m")) + require.Equal(t, int(0.5*1000000000), ConvertToBytes("0.5g")) + + // Test space handling + require.Equal(t, 100*1000, ConvertToBytes("100 k")) + require.Equal(t, 100*1000, ConvertToBytes("100 k")) // multiple spaces +} + +func Test_ConvertToBytes_DigitDetection(t *testing.T) { + t.Parallel() + // Test the new direct byte comparison digit detection + testCases := []struct { + input string + expected int + desc string + }{ + {"0", 0, "digit 0"}, + {"1", 1, "digit 1"}, + {"9", 9, "digit 9"}, + {"123", 123, "multiple digits"}, + {"123k", 123000, "digits with unit"}, + {"a123", 0, "non-digit start"}, + {"12a3", 0, "non-digit in middle stops parsing"}, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + + require.Equal(t, tc.expected, ConvertToBytes(tc.input), "input: %s", tc.input) + }) + } } func Test_GetArgument(t *testing.T) { diff --git a/go.mod b/go.mod index 7edf58b..49412df 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,7 @@ module github.com/gofiber/utils/v2 go 1.23.0 + require ( github.com/fxamacker/cbor/v2 v2.8.0 github.com/google/uuid v1.6.0 diff --git a/http.go b/http.go index ed80825..19e7fe2 100644 --- a/http.go +++ b/http.go @@ -41,14 +41,14 @@ func GetMIME(extension string) string { // if it is parsable to any known types. If its not vendor specific then returns // the original content type. func ParseVendorSpecificContentType(cType string) string { - plusIndex := strings.Index(cType, "+") + plusIndex := strings.IndexByte(cType, '+') if plusIndex == -1 { return cType } var parsableType string - if semiColonIndex := strings.Index(cType, ";"); semiColonIndex == -1 { + if semiColonIndex := strings.IndexByte(cType, ';'); semiColonIndex == -1 { parsableType = cType[plusIndex+1:] } else if plusIndex < semiColonIndex { parsableType = cType[plusIndex+1 : semiColonIndex] @@ -56,7 +56,7 @@ func ParseVendorSpecificContentType(cType string) string { return cType[:semiColonIndex] } - slashIndex := strings.Index(cType, "/") + slashIndex := strings.IndexByte(cType, '/') if slashIndex == -1 { return cType diff --git a/http_test.go b/http_test.go index addc070..f1fd1a5 100644 --- a/http_test.go +++ b/http_test.go @@ -75,6 +75,44 @@ func Test_ParseVendorSpecificContentType(t *testing.T) { cType := ParseVendorSpecificContentType("application/json") require.Equal(t, "application/json", cType) + // Test with parameters (semicolon IndexByte optimization) + cType = ParseVendorSpecificContentType("multipart/form-data; boundary=abc123") + require.Equal(t, "multipart/form-data; boundary=abc123", cType) + + // Test vendor-specific content types (plus IndexByte optimization) + cType = ParseVendorSpecificContentType("application/vnd.api+json; version=1") + require.Equal(t, "application/json", cType) + cType = ParseVendorSpecificContentType("application/vnd.dummy+x-www-form-urlencoded") + require.Equal(t, "application/x-www-form-urlencoded", cType) + + // Test invalid cases (slash IndexByte optimization) + cType = ParseVendorSpecificContentType("something invalid") + require.Equal(t, "something invalid", cType) + + // Additional edge cases for IndexByte optimization + cType = ParseVendorSpecificContentType("application/vnd.custom+xml; charset=utf-8") + require.Equal(t, "application/xml", cType) + + cType = ParseVendorSpecificContentType("text/vnd.example+plain") + require.Equal(t, "text/plain", cType) + + cType = ParseVendorSpecificContentType("application/vnd.test+json;boundary=test") + require.Equal(t, "application/json", cType) + + // Edge cases with multiple special characters + cType = ParseVendorSpecificContentType("application/vnd.api+json+extra; param=value") + require.Equal(t, "application/json+extra", cType) + + // Semicolon before plus + cType = ParseVendorSpecificContentType("application/json; charset=utf-8+extra") + require.Equal(t, "application/json", cType) + + // Empty and single-character inputs + require.Equal(t, "", ParseVendorSpecificContentType("")) + require.Equal(t, "+", ParseVendorSpecificContentType("+")) + require.Equal(t, ";", ParseVendorSpecificContentType(";")) + require.Equal(t, "/", ParseVendorSpecificContentType("/")) + cType = ParseVendorSpecificContentType("multipart/form-data; boundary=dart-http-boundary-ZnVy.ICWq+7HOdsHqWxCFa8g3D.KAhy+Y0sYJ_lBADypu8po3_X") require.Equal(t, "multipart/form-data", cType) @@ -97,6 +135,32 @@ func Test_ParseVendorSpecificContentType(t *testing.T) { require.Equal(t, "invalid+withoutSlash", cType) } +func Test_ParseVendorSpecificContentType_IndexByteOptimization(t *testing.T) { + t.Parallel() + testCases := []struct { + input string + expected string + desc string + }{ + {"application/vnd.api+json", "application/json", "plus in middle"}, + {"+json", "+json", "plus at start, no slash"}, + {"application/+json", "application/json", "plus after slash"}, + {"application/json;charset=utf-8", "application/json;charset=utf-8", "semicolon after content type"}, + {";charset=utf-8", ";charset=utf-8", "semicolon at start"}, + {"application/vnd.api+json;version=1", "application/json", "plus before semicolon"}, + {"application/json", "application/json", "normal content type with slash"}, + {"applicationjson", "applicationjson", "no slash in content type"}, + {"app/vnd.test+data/extra", "app/data/extra", "multiple slashes"}, + } + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + t.Parallel() + + require.Equal(t, tc.expected, ParseVendorSpecificContentType(tc.input), "input: %s", tc.input) + }) + } +} + func Benchmark_ParseVendorSpecificContentType(b *testing.B) { b.Run("vendorContentType", func(b *testing.B) { for n := 0; n < b.N; n++ { diff --git a/strings.go b/strings.go index d05c5f6..165c80b 100644 --- a/strings.go +++ b/strings.go @@ -6,6 +6,10 @@ package utils // ToLower converts ascii string to lower-case func ToLower(b string) string { + if len(b) == 0 { + return b + } + res := make([]byte, len(b)) copy(res, b) for i := 0; i < len(res); i++ { @@ -17,6 +21,10 @@ func ToLower(b string) string { // ToUpper converts ascii string to upper-case func ToUpper(b string) string { + if len(b) == 0 { + return b + } + res := make([]byte, len(b)) copy(res, b) for i := 0; i < len(res); i++ { diff --git a/strings_test.go b/strings_test.go index f837b2d..cbd65e4 100644 --- a/strings_test.go +++ b/strings_test.go @@ -13,6 +13,8 @@ import ( func Test_ToUpper(t *testing.T) { t.Parallel() + // Test empty string early return optimization + require.Equal(t, "", ToUpper("")) require.Equal(t, "/MY/NAME/IS/:PARAM/*", ToUpper("/my/name/is/:param/*")) } @@ -52,11 +54,18 @@ func Benchmark_ToUpper(b *testing.B) { func Test_ToLower(t *testing.T) { t.Parallel() + // Test empty string early return optimization + require.Equal(t, "", ToLower("")) require.Equal(t, "/my/name/is/:param/*", ToLower("/MY/NAME/IS/:PARAM/*")) require.Equal(t, "/my1/name/is/:param/*", ToLower("/MY1/NAME/IS/:PARAM/*")) require.Equal(t, "/my2/name/is/:param/*", ToLower("/MY2/NAME/IS/:PARAM/*")) require.Equal(t, "/my3/name/is/:param/*", ToLower("/MY3/NAME/IS/:PARAM/*")) require.Equal(t, "/my4/name/is/:param/*", ToLower("/MY4/NAME/IS/:PARAM/*")) + // Test single character optimizations + require.Equal(t, "a", ToLower("A")) + require.Equal(t, "z", ToLower("Z")) + require.Equal(t, "1", ToLower("1")) // non-letter should remain unchanged + require.Equal(t, "!", ToLower("!")) // special character should remain unchanged } func Benchmark_ToLower(b *testing.B) { @@ -125,3 +134,47 @@ func Benchmark_IfToToLower_HeadersOrigin(b *testing.B) { require.Equal(b, "https://gofiber.io", res) }) } + +func Test_ToLower_DirectByteIteration(t *testing.T) { + t.Parallel() + // Test various ASCII characters to ensure direct byte iteration works correctly + testCases := []struct { + input string + expected string + }{ + {"ABC123!@#", "abc123!@#"}, + {"MiXeD cAsE", "mixed case"}, + {"ALLUPPERCASE", "alluppercase"}, + {"alllowercase", "alllowercase"}, + {"Numbers123AndSymbols!@#", "numbers123andsymbols!@#"}, + } + for _, tc := range testCases { + t.Run(tc.input, func(t *testing.T) { + t.Parallel() + + require.Equal(t, tc.expected, ToLower(tc.input)) + }) + } +} + +func Test_ToUpper_DirectByteIteration(t *testing.T) { + t.Parallel() + // Test various ASCII characters to ensure direct byte iteration works correctly + testCases := []struct { + input string + expected string + }{ + {"abc123!@#", "ABC123!@#"}, + {"MiXeD cAsE", "MIXED CASE"}, + {"ALLUPPERCASE", "ALLUPPERCASE"}, + {"alllowercase", "ALLLOWERCASE"}, + {"Numbers123AndSymbols!@#", "NUMBERS123ANDSYMBOLS!@#"}, + } + for _, tc := range testCases { + t.Run(tc.input, func(t *testing.T) { + t.Parallel() + + require.Equal(t, tc.expected, ToUpper(tc.input)) + }) + } +}