Skip to content

Commit fe1bf4a

Browse files
feat(ctx): add conditional copy helpers (#3703)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
1 parent 64a7113 commit fe1bf4a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+261
-157
lines changed

app.go

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ import (
2424
"strings"
2525
"sync"
2626
"time"
27+
"unsafe"
2728

28-
"github.com/gofiber/utils/v2"
29+
utils "github.com/gofiber/utils/v2"
2930
"github.com/valyala/fasthttp"
3031

3132
"github.com/gofiber/fiber/v3/binder"
@@ -74,9 +75,9 @@ type App struct {
7475
// Fasthttp server
7576
server *fasthttp.Server
7677
// Converts string to a byte slice
77-
getBytes func(s string) (b []byte)
78+
toBytes func(s string) (b []byte)
7879
// Converts byte slice to a string
79-
getString func(b []byte) string
80+
toString func(b []byte) string
8081
// Hooks
8182
hooks *Hooks
8283
// Latest route & group
@@ -516,8 +517,8 @@ func New(config ...Config) *App {
516517
app := &App{
517518
// Create config
518519
config: Config{},
519-
getBytes: utils.UnsafeBytes,
520-
getString: utils.UnsafeString,
520+
toBytes: utils.UnsafeBytes,
521+
toString: utils.UnsafeString,
521522
latestRoute: &Route{},
522523
customBinders: []CustomBinder{},
523524
sendfiles: []*sendFileStore{},
@@ -572,7 +573,7 @@ func New(config ...Config) *App {
572573
}
573574

574575
if app.config.Immutable {
575-
app.getBytes, app.getString = getBytesImmutable, getStringImmutable
576+
app.toBytes, app.toString = toBytesImmutable, toStringImmutable
576577
}
577578

578579
if app.config.ErrorHandler == nil {
@@ -635,6 +636,30 @@ func NewWithCustomCtx(newCtxFunc func(app *App) CustomCtx, config ...Config) *Ap
635636
return app
636637
}
637638

639+
// GetString returns s unchanged when Immutable is off or s is read-only (rodata).
640+
// Otherwise it returns a detached copy (strings.Clone).
641+
func (app *App) GetString(s string) string {
642+
if !app.config.Immutable || len(s) == 0 {
643+
return s
644+
}
645+
if isReadOnly(unsafe.Pointer(unsafe.StringData(s))) { //nolint:gosec // pointer check avoids unnecessary copy
646+
return s // literal / rodata → safe to return as-is
647+
}
648+
return strings.Clone(s) // heap-backed / aliased → detach
649+
}
650+
651+
// GetBytes returns b unchanged when Immutable is off or b is read-only (rodata).
652+
// Otherwise it returns a detached copy.
653+
func (app *App) GetBytes(b []byte) []byte {
654+
if !app.config.Immutable || len(b) == 0 {
655+
return b
656+
}
657+
if isReadOnly(unsafe.Pointer(unsafe.SliceData(b))) { //nolint:gosec // pointer check avoids unnecessary copy
658+
return b // rodata → safe to return as-is
659+
}
660+
return utils.CopyBytes(b) // detach when backed by request/response memory
661+
}
662+
638663
// Adds an ip address to TrustProxyConfig.ranges or TrustProxyConfig.ips based on whether it is an IP range or not
639664
func (app *App) handleTrustedProxy(ipAddress string) {
640665
if strings.Contains(ipAddress, "/") {

app_test.go

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,9 @@ import (
2626
"sync"
2727
"testing"
2828
"time"
29+
"unsafe"
2930

30-
"github.com/gofiber/utils/v2"
31+
utils "github.com/gofiber/utils/v2"
3132

3233
"github.com/stretchr/testify/assert"
3334
"github.com/stretchr/testify/require"
@@ -589,7 +590,7 @@ func Test_App_Use_UnescapedPath(t *testing.T) {
589590
body, err := io.ReadAll(resp.Body)
590591
require.NoError(t, err, "app.Test(req)")
591592
// check the param result
592-
require.Equal(t, "اختبار", app.getString(body))
593+
require.Equal(t, "اختبار", app.toString(body))
593594

594595
// with lowercase letters
595596
resp, err = app.Test(httptest.NewRequest(MethodGet, "/cr%C3%A9er/%D8%A7%D8%AE%D8%AA%D8%A8%D8%A7%D8%B1", nil))
@@ -625,7 +626,7 @@ func Test_App_Use_CaseSensitive(t *testing.T) {
625626
body, err := io.ReadAll(resp.Body)
626627
require.NoError(t, err, "app.Test(req)")
627628
// check the detected path result
628-
require.Equal(t, "/AbC", app.getString(body))
629+
require.Equal(t, "/AbC", app.toString(body))
629630
}
630631

631632
func Test_App_Not_Use_StrictRouting(t *testing.T) {
@@ -981,6 +982,56 @@ func Test_App_Config(t *testing.T) {
981982
require.True(t, app.Config().StrictRouting)
982983
}
983984

985+
func Test_App_GetString(t *testing.T) {
986+
t.Parallel()
987+
988+
heap := string([]byte("fiber"))
989+
appMutable := New()
990+
same := appMutable.GetString(heap)
991+
if unsafe.StringData(same) != unsafe.StringData(heap) { //nolint:gosec // compare pointer addresses
992+
t.Errorf("expected original string when immutable is disabled")
993+
}
994+
995+
appImmutable := New(Config{Immutable: true})
996+
copied := appImmutable.GetString(heap)
997+
if unsafe.StringData(copied) == unsafe.StringData(heap) { //nolint:gosec // compare pointer addresses
998+
t.Errorf("expected a copy for heap-backed string when immutable is enabled")
999+
}
1000+
1001+
literal := "fiber"
1002+
sameLit := appImmutable.GetString(literal)
1003+
if unsafe.StringData(sameLit) != unsafe.StringData(literal) { //nolint:gosec // compare pointer addresses
1004+
t.Errorf("expected original literal when immutable is enabled")
1005+
}
1006+
}
1007+
1008+
func Test_App_GetBytes(t *testing.T) {
1009+
t.Parallel()
1010+
1011+
b := []byte("fiber")
1012+
appMutable := New()
1013+
same := appMutable.GetBytes(b)
1014+
if unsafe.SliceData(same) != unsafe.SliceData(b) {
1015+
t.Errorf("expected original slice when immutable is disabled")
1016+
}
1017+
1018+
alias := make([]byte, 10)
1019+
copy(alias, b)
1020+
sub := alias[:5]
1021+
appImmutable := New(Config{Immutable: true})
1022+
copied := appImmutable.GetBytes(sub)
1023+
if unsafe.SliceData(copied) == unsafe.SliceData(sub) {
1024+
t.Errorf("expected a copy for aliased slice when immutable is enabled")
1025+
}
1026+
1027+
full := make([]byte, 5)
1028+
copy(full, b)
1029+
detached := appImmutable.GetBytes(full)
1030+
if unsafe.SliceData(detached) == unsafe.SliceData(full) {
1031+
t.Errorf("expected a copy even when cap==len")
1032+
}
1033+
}
1034+
9841035
func Test_App_Shutdown(t *testing.T) {
9851036
t.Parallel()
9861037
t.Run("success", func(t *testing.T) {

bind.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"sync"
88

99
"github.com/gofiber/fiber/v3/binder"
10-
"github.com/gofiber/utils/v2"
10+
utils "github.com/gofiber/utils/v2"
1111
)
1212

1313
// CustomBinder An interface to register custom binders.

binder/cbor.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package binder
22

33
import (
4-
"github.com/gofiber/utils/v2"
4+
utils "github.com/gofiber/utils/v2"
55
)
66

77
// CBORBinding is the CBOR binder for CBOR request body.

binder/cookie.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package binder
22

33
import (
4-
"github.com/gofiber/utils/v2"
4+
utils "github.com/gofiber/utils/v2"
55
"github.com/valyala/fasthttp"
66
)
77

binder/form.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ package binder
33
import (
44
"mime/multipart"
55

6-
"github.com/gofiber/utils/v2"
6+
utils "github.com/gofiber/utils/v2"
77
"github.com/valyala/fasthttp"
88
)
99

binder/header.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package binder
22

33
import (
4-
"github.com/gofiber/utils/v2"
4+
utils "github.com/gofiber/utils/v2"
55
"github.com/valyala/fasthttp"
66
)
77

binder/json.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package binder
22

33
import (
4-
"github.com/gofiber/utils/v2"
4+
utils "github.com/gofiber/utils/v2"
55
)
66

77
// JSONBinding is the JSON binder for JSON request body.

binder/mapping.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"strings"
1010
"sync"
1111

12-
"github.com/gofiber/utils/v2"
12+
utils "github.com/gofiber/utils/v2"
1313
"github.com/valyala/bytebufferpool"
1414

1515
"github.com/gofiber/schema"

binder/msgpack.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package binder
22

33
import (
4-
"github.com/gofiber/utils/v2"
4+
utils "github.com/gofiber/utils/v2"
55
)
66

77
// MsgPackBinding is the MsgPack binder for MsgPack request body.

0 commit comments

Comments
 (0)