Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 9 additions & 9 deletions common.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@ import (
"crypto/rand"
"encoding/binary"
"encoding/hex"
"math"
"net"
"os"
"reflect"
"runtime"
"strconv"
"sync"
"sync/atomic"
"unicode"

"github.com/google/uuid"
)
Expand All @@ -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)
Expand Down Expand Up @@ -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
}
}
Expand All @@ -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]
}
}

Expand Down
74 changes: 73 additions & 1 deletion common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,26 +75,98 @@ 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"))
require.Equal(t, 42*1000, ConvertToBytes("42KB"))
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) {
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 3 additions & 3 deletions http.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,22 +41,22 @@ 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]
} else {
return cType[:semiColonIndex]
}

slashIndex := strings.Index(cType, "/")
slashIndex := strings.IndexByte(cType, '/')

if slashIndex == -1 {
return cType
Expand Down
64 changes: 64 additions & 0 deletions http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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++ {
Expand Down
8 changes: 8 additions & 0 deletions strings.go
Original file line number Diff line number Diff line change
Expand Up @@ -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++ {
Expand All @@ -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++ {
Expand Down
53 changes: 53 additions & 0 deletions strings_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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/*"))
}

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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))
})
}
}
Loading