mirror of
https://github.com/gin-gonic/gin.git
synced 2025-12-03 21:44:33 +08:00
perf(path): replace regex with custom functions in redirectTrailingSlash (#4414)
* perf: replace regex with custom functions in redirectTrailingSlash * perf: use more efficient removeRepeatedChar for path slash handling --------- Co-authored-by: 1911860538 <alxps1911@gmail.com>
This commit is contained in:
parent
ecb3f7b5e2
commit
440eb14ab8
21
gin.go
21
gin.go
@ -11,7 +11,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
@ -48,11 +47,6 @@ var defaultTrustedCIDRs = []*net.IPNet{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
|
||||||
regSafePrefix = regexp.MustCompile("[^a-zA-Z0-9/-]+")
|
|
||||||
regRemoveRepeatedChar = regexp.MustCompile("/{2,}")
|
|
||||||
)
|
|
||||||
|
|
||||||
// HandlerFunc defines the handler used by gin middleware as return value.
|
// HandlerFunc defines the handler used by gin middleware as return value.
|
||||||
type HandlerFunc func(*Context)
|
type HandlerFunc func(*Context)
|
||||||
|
|
||||||
@ -776,8 +770,8 @@ func redirectTrailingSlash(c *Context) {
|
|||||||
req := c.Request
|
req := c.Request
|
||||||
p := req.URL.Path
|
p := req.URL.Path
|
||||||
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
|
if prefix := path.Clean(c.Request.Header.Get("X-Forwarded-Prefix")); prefix != "." {
|
||||||
prefix = regSafePrefix.ReplaceAllString(prefix, "")
|
prefix = sanitizePathChars(prefix)
|
||||||
prefix = regRemoveRepeatedChar.ReplaceAllString(prefix, "/")
|
prefix = removeRepeatedChar(prefix, '/')
|
||||||
|
|
||||||
p = prefix + "/" + req.URL.Path
|
p = prefix + "/" + req.URL.Path
|
||||||
}
|
}
|
||||||
@ -788,6 +782,17 @@ func redirectTrailingSlash(c *Context) {
|
|||||||
redirectRequest(c)
|
redirectRequest(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sanitizePathChars removes unsafe characters from path strings,
|
||||||
|
// keeping only ASCII letters, ASCII numbers, forward slashes, and hyphens.
|
||||||
|
func sanitizePathChars(s string) string {
|
||||||
|
return strings.Map(func(r rune) rune {
|
||||||
|
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '/' || r == '-' {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}, s)
|
||||||
|
}
|
||||||
|
|
||||||
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
|
func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool {
|
||||||
req := c.Request
|
req := c.Request
|
||||||
rPath := req.URL.Path
|
rPath := req.URL.Path
|
||||||
|
|||||||
55
path.go
55
path.go
@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
package gin
|
package gin
|
||||||
|
|
||||||
|
const stackBufSize = 128
|
||||||
|
|
||||||
// cleanPath is the URL version of path.Clean, it returns a canonical URL path
|
// cleanPath is the URL version of path.Clean, it returns a canonical URL path
|
||||||
// for p, eliminating . and .. elements.
|
// for p, eliminating . and .. elements.
|
||||||
//
|
//
|
||||||
@ -19,7 +21,6 @@ package gin
|
|||||||
//
|
//
|
||||||
// If the result of this process is an empty string, "/" is returned.
|
// If the result of this process is an empty string, "/" is returned.
|
||||||
func cleanPath(p string) string {
|
func cleanPath(p string) string {
|
||||||
const stackBufSize = 128
|
|
||||||
// Turn empty string into "/"
|
// Turn empty string into "/"
|
||||||
if p == "" {
|
if p == "" {
|
||||||
return "/"
|
return "/"
|
||||||
@ -148,3 +149,55 @@ func bufApp(buf *[]byte, s string, w int, c byte) {
|
|||||||
}
|
}
|
||||||
b[w] = c
|
b[w] = c
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// removeRepeatedChar removes multiple consecutive 'char's from a string.
|
||||||
|
// if s == "/a//b///c////" && char == '/', it returns "/a/b/c/"
|
||||||
|
func removeRepeatedChar(s string, char byte) string {
|
||||||
|
// Check if there are any consecutive chars
|
||||||
|
hasRepeatedChar := false
|
||||||
|
for i := 1; i < len(s); i++ {
|
||||||
|
if s[i] == char && s[i-1] == char {
|
||||||
|
hasRepeatedChar = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasRepeatedChar {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reasonably sized buffer on stack to avoid allocations in the common case.
|
||||||
|
buf := make([]byte, 0, stackBufSize)
|
||||||
|
|
||||||
|
// Invariants:
|
||||||
|
// reading from s; r is index of next byte to process.
|
||||||
|
// writing to buf; w is index of next byte to write.
|
||||||
|
r := 0
|
||||||
|
w := 0
|
||||||
|
|
||||||
|
for n := len(s); r < n; {
|
||||||
|
if s[r] == char {
|
||||||
|
// Write the first char
|
||||||
|
bufApp(&buf, s, w, char)
|
||||||
|
w++
|
||||||
|
r++
|
||||||
|
|
||||||
|
// Skip all consecutive chars
|
||||||
|
for r < n && s[r] == char {
|
||||||
|
r++
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Copy non-char character
|
||||||
|
bufApp(&buf, s, w, s[r])
|
||||||
|
w++
|
||||||
|
r++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the original string was not modified (or only shortened at the end),
|
||||||
|
// return the respective substring of the original string.
|
||||||
|
// Otherwise, return a new string from the buffer.
|
||||||
|
if len(buf) == 0 {
|
||||||
|
return s[:w]
|
||||||
|
}
|
||||||
|
return string(buf[:w])
|
||||||
|
}
|
||||||
|
|||||||
47
path_test.go
47
path_test.go
@ -143,3 +143,50 @@ func BenchmarkPathCleanLong(b *testing.B) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRemoveRepeatedChar(t *testing.T) {
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
str string
|
||||||
|
char byte
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty",
|
||||||
|
str: "",
|
||||||
|
char: 'a',
|
||||||
|
want: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "noSlash",
|
||||||
|
str: "abc",
|
||||||
|
char: ',',
|
||||||
|
want: "abc",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "withSlash",
|
||||||
|
str: "/a/b/c/",
|
||||||
|
char: '/',
|
||||||
|
want: "/a/b/c/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "withRepeatedSlashes",
|
||||||
|
str: "/a//b///c////",
|
||||||
|
char: '/',
|
||||||
|
want: "/a/b/c/",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "threeSlashes",
|
||||||
|
str: "///",
|
||||||
|
char: '/',
|
||||||
|
want: "/",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
res := removeRepeatedChar(tc.str, tc.char)
|
||||||
|
assert.Equal(t, tc.want, res)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user