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
696 changes: 343 additions & 353 deletions README.md

Large diffs are not rendered by default.

68 changes: 14 additions & 54 deletions bytes.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,64 +4,24 @@

package utils

// ToLowerBytes converts ascii slice to lower-case
import (
casebytes "github.com/gofiber/utils/v2/bytes"
)

// ToLowerBytes converts an ASCII byte slice to lower-case in-place.
//
// Deprecated: use package "github.com/gofiber/utils/v2/bytes" and call bytes.UnsafeToLower.
// This wrapper keeps backward compatibility by mutating the provided slice.
func ToLowerBytes(b []byte) []byte {
table := toLowerTable
n := len(b)
i := 0

// Unroll by 4 to balance instruction-level parallelism with cache pressure.
limit := n &^ 3
for i < limit {
b0 := b[i+0]
b1 := b[i+1]
b2 := b[i+2]
b3 := b[i+3]

b[i+0] = table[b0]
b[i+1] = table[b1]
b[i+2] = table[b2]
b[i+3] = table[b3]

i += 4
}

for i < n {
b[i] = table[b[i]]
i++
}

return b
return casebytes.UnsafeToLower(b)
}
Comment thread
ReneWerner87 marked this conversation as resolved.

// ToUpperBytes converts ascii slice to upper-case
// ToUpperBytes converts an ASCII byte slice to upper-case in-place.
//
// Deprecated: use package "github.com/gofiber/utils/v2/bytes" and call bytes.UnsafeToUpper.
// This wrapper keeps backward compatibility by mutating the provided slice.
func ToUpperBytes(b []byte) []byte {
table := toUpperTable
n := len(b)
i := 0

// Unroll by 4 to match ToLowerBytes and maximize throughput on amd64.
limit := n &^ 3
for i < limit {
b0 := b[i+0]
b1 := b[i+1]
b2 := b[i+2]
b3 := b[i+3]

b[i+0] = table[b0]
b[i+1] = table[b1]
b[i+2] = table[b2]
b[i+3] = table[b3]

i += 4
}

for i < n {
b[i] = table[b[i]]
i++
}

return b
return casebytes.UnsafeToUpper(b)
}

// AddTrailingSlashBytes appends a trailing '/' to b if it does not already end with one.
Expand Down
131 changes: 131 additions & 0 deletions bytes/case.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package bytes

import (
"github.com/gofiber/utils/v2/internal/caseconv"
)

// ToLower converts an ASCII byte slice to lower-case without modifying the input.
func ToLower(b []byte) []byte {
n := len(b)
if n == 0 {
return b
}

table := caseconv.ToLowerTable
i := 0
for i < n {
Comment thread
ReneWerner87 marked this conversation as resolved.
c := b[i]
low := table[c]
if low != c {
dst := make([]byte, n)
copy(dst, b[:i])
dst[i] = low
i++
limit := i + ((n - i) &^ 3)
for i < limit {
dst[i+0] = table[b[i+0]]
dst[i+1] = table[b[i+1]]
dst[i+2] = table[b[i+2]]
dst[i+3] = table[b[i+3]]
i += 4
}
for i < n {
dst[i] = table[b[i]]
i++
}
return dst
}
i++
}
return b
Comment thread
ReneWerner87 marked this conversation as resolved.
}

// ToUpper converts an ASCII byte slice to upper-case without modifying the input.
func ToUpper(b []byte) []byte {
n := len(b)
if n == 0 {
return b
}

table := caseconv.ToUpperTable
i := 0
for i < n {
c := b[i]
up := table[c]
if up != c {
dst := make([]byte, n)
copy(dst, b[:i])
dst[i] = up
i++
limit := i + ((n - i) &^ 3)
for i < limit {
dst[i+0] = table[b[i+0]]
dst[i+1] = table[b[i+1]]
dst[i+2] = table[b[i+2]]
dst[i+3] = table[b[i+3]]
i += 4
}
for i < n {
dst[i] = table[b[i]]
i++
}
return dst
}
i++
}
return b
}

// UnsafeToLower converts an ASCII byte slice to lower-case in-place.
// The passed slice content is modified and the same slice is returned.
func UnsafeToLower(b []byte) []byte {
table := caseconv.ToLowerTable
n := len(b)
i := 0
limit := n &^ 3
for i < limit {
b0 := b[i+0]
b1 := b[i+1]
b2 := b[i+2]
b3 := b[i+3]

b[i+0] = table[b0]
b[i+1] = table[b1]
b[i+2] = table[b2]
b[i+3] = table[b3]

i += 4
}
for i < n {
b[i] = table[b[i]]
i++
}
return b
}

// UnsafeToUpper converts an ASCII byte slice to upper-case in-place.
// The passed slice content is modified and the same slice is returned.
func UnsafeToUpper(b []byte) []byte {
table := caseconv.ToUpperTable
n := len(b)
i := 0
limit := n &^ 3
for i < limit {
b0 := b[i+0]
b1 := b[i+1]
b2 := b[i+2]
b3 := b[i+3]

b[i+0] = table[b0]
b[i+1] = table[b1]
b[i+2] = table[b2]
b[i+3] = table[b3]

i += 4
}
for i < n {
b[i] = table[b[i]]
i++
}
return b
}
183 changes: 183 additions & 0 deletions bytes/case_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
package bytes

import (
stdbytes "bytes"
"strings"
"testing"

"github.com/stretchr/testify/require"
)

type benchCase struct {
name string
input string
lower string
upper string
}

var benchmarkCoreCases = []benchCase{
{name: "empty", input: "", upper: "", lower: ""},
{name: "http-get", input: "get", upper: "GET", lower: "get"},
{name: "http-get-upper", input: "GET", upper: "GET", lower: "get"},
{
name: "header-content-type-mixed",
input: "Content-Type: text/html; charset=utf-8",
upper: "CONTENT-TYPE: TEXT/HTML; CHARSET=UTF-8",
lower: "content-type: text/html; charset=utf-8",
},
{name: "large-lower", input: strings.Repeat("a", 64), upper: strings.Repeat("A", 64), lower: strings.Repeat("a", 64)},
{name: "large-upper", input: strings.Repeat("A", 64), upper: strings.Repeat("A", 64), lower: strings.Repeat("a", 64)},
{name: "large-mixed", input: strings.Repeat("aB", 32), upper: strings.Repeat("AB", 32), lower: strings.Repeat("ab", 32)},
}

func Test_ToLowerBytes(t *testing.T) {
t.Parallel()

require.Equal(t, []byte("/my/name/is/:param/*"), UnsafeToLower([]byte("/MY/NAME/IS/:PARAM/*")))
require.Equal(t, []byte("/my1/name/is/:param/*"), UnsafeToLower([]byte("/MY1/NAME/IS/:PARAM/*")))
require.Equal(t, []byte("/my2/name/is/:param/*"), UnsafeToLower([]byte("/MY2/NAME/IS/:PARAM/*")))
require.Equal(t, []byte("/my3/name/is/:param/*"), UnsafeToLower([]byte("/MY3/NAME/IS/:PARAM/*")))
require.Equal(t, []byte("/my4/name/is/:param/*"), UnsafeToLower([]byte("/MY4/NAME/IS/:PARAM/*")))
}

func Test_ToUpperBytes(t *testing.T) {
t.Parallel()

require.Equal(t, []byte("/MY/NAME/IS/:PARAM/*"), UnsafeToUpper([]byte("/my/name/is/:param/*")))
require.Equal(t, []byte("/MY1/NAME/IS/:PARAM/*"), UnsafeToUpper([]byte("/my1/name/is/:param/*")))
require.Equal(t, []byte("/MY2/NAME/IS/:PARAM/*"), UnsafeToUpper([]byte("/my2/name/is/:param/*")))
require.Equal(t, []byte("/MY3/NAME/IS/:PARAM/*"), UnsafeToUpper([]byte("/my3/name/is/:param/*")))
require.Equal(t, []byte("/MY4/NAME/IS/:PARAM/*"), UnsafeToUpper([]byte("/my4/name/is/:param/*")))
}

func Test_ToLower_NoMutation(t *testing.T) {
in := []byte("Content-Type")
snapshot := append([]byte(nil), in...)
out := ToLower(in)

require.Equal(t, "content-type", string(out))
require.Equal(t, snapshot, in)
}

func Test_ToUpper_NoMutation(t *testing.T) {
in := []byte("content-type")
snapshot := append([]byte(nil), in...)
out := ToUpper(in)

require.Equal(t, "CONTENT-TYPE", string(out))
require.Equal(t, snapshot, in)
}

func TestUnsafeToLower_Mutates(t *testing.T) {
in := []byte("Content-Type")
out := UnsafeToLower(in)
require.Same(t, &out[0], &in[0])
require.Equal(t, "content-type", string(in))
}

func TestUnsafeToUpper_Mutates(t *testing.T) {
in := []byte("content-type")
out := UnsafeToUpper(in)
require.Same(t, &out[0], &in[0])
require.Equal(t, "CONTENT-TYPE", string(in))
}

func Test_ToLowerBytes_Edge(t *testing.T) {
t.Parallel()

cases := [][]byte{
{},
[]byte("123"),
[]byte("!@#"),
}
for _, c := range cases {
t.Run(string(c), func(t *testing.T) {
t.Parallel()
require.Equal(t, stdbytes.ToLower(c), UnsafeToLower(c))
})
}
}

func Test_ToUpperBytes_Edge(t *testing.T) {
t.Parallel()

cases := [][]byte{
{},
[]byte("123"),
[]byte("!@#"),
}
for _, c := range cases {
t.Run(string(c), func(t *testing.T) {
t.Parallel()
require.Equal(t, stdbytes.ToUpper(c), UnsafeToUpper(c))
})
}
}

func Benchmark_ToLowerBytes(b *testing.B) {
for _, tc := range benchmarkCoreCases {
b.Run(tc.name, func(b *testing.B) {
template := []byte(tc.input)
want := []byte(tc.lower)
var res []byte

b.Run("fiber", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
res = ToLower(template)
}
require.Equal(b, want, res)
})
b.Run("fiber/unsafe", func(b *testing.B) {
b.ReportAllocs()
work := make([]byte, len(template))
for i := 0; i < b.N; i++ {
copy(work, template)
res = UnsafeToLower(work)
}
require.Equal(b, want, res)
})
b.Run("default", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
res = stdbytes.ToLower(template)
}
require.Equal(b, want, res)
})
})
}
}

func Benchmark_ToUpperBytes(b *testing.B) {
for _, tc := range benchmarkCoreCases {
b.Run(tc.name, func(b *testing.B) {
template := []byte(tc.input)
want := []byte(tc.upper)
var res []byte

b.Run("fiber", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
res = ToUpper(template)
}
require.Equal(b, want, res)
})
b.Run("fiber/unsafe", func(b *testing.B) {
b.ReportAllocs()
work := make([]byte, len(template))
for i := 0; i < b.N; i++ {
copy(work, template)
res = UnsafeToUpper(work)
}
require.Equal(b, want, res)
})
b.Run("default", func(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
res = stdbytes.ToUpper(template)
}
require.Equal(b, want, res)
})
})
}
}
Loading
Loading