mirror of
https://github.com/gin-gonic/gin.git
synced 2025-10-15 04:57:07 +08:00
Merge branch 'gin-gonic:master' into master
This commit is contained in:
commit
386c68bd8b
@ -193,14 +193,25 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
|
|||||||
if !ok {
|
if !ok {
|
||||||
vs = []string{opt.defaultValue}
|
vs = []string{opt.defaultValue}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ok, err = trySetCustom(vs[0], value); ok {
|
||||||
|
return ok, err
|
||||||
|
}
|
||||||
|
|
||||||
return true, setSlice(vs, value, field)
|
return true, setSlice(vs, value, field)
|
||||||
case reflect.Array:
|
case reflect.Array:
|
||||||
if !ok {
|
if !ok {
|
||||||
vs = []string{opt.defaultValue}
|
vs = []string{opt.defaultValue}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ok, err = trySetCustom(vs[0], value); ok {
|
||||||
|
return ok, err
|
||||||
|
}
|
||||||
|
|
||||||
if len(vs) != value.Len() {
|
if len(vs) != value.Len() {
|
||||||
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
|
return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
|
||||||
}
|
}
|
||||||
|
|
||||||
return true, setArray(vs, value, field)
|
return true, setArray(vs, value, field)
|
||||||
default:
|
default:
|
||||||
var val string
|
var val string
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
package binding
|
package binding
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/hex"
|
||||||
"fmt"
|
"fmt"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"reflect"
|
"reflect"
|
||||||
@ -422,3 +423,89 @@ func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
|
|||||||
assert.EqualValues(t, "/foo", s.FileData.Path)
|
assert.EqualValues(t, "/foo", s.FileData.Path)
|
||||||
assert.EqualValues(t, "happiness", s.FileData.Name)
|
assert.EqualValues(t, "happiness", s.FileData.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type customPath []string
|
||||||
|
|
||||||
|
func (p *customPath) UnmarshalParam(param string) error {
|
||||||
|
elems := strings.Split(param, "/")
|
||||||
|
n := len(elems)
|
||||||
|
if n < 2 {
|
||||||
|
return fmt.Errorf("invalid format")
|
||||||
|
}
|
||||||
|
|
||||||
|
*p = elems
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomSliceUri(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
FileData customPath `uri:"path"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "uri")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, "bar", s.FileData[0])
|
||||||
|
assert.EqualValues(t, "foo", s.FileData[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomSliceForm(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
FileData customPath `form:"path"`
|
||||||
|
}
|
||||||
|
err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
assert.EqualValues(t, "bar", s.FileData[0])
|
||||||
|
assert.EqualValues(t, "foo", s.FileData[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
type objectID [12]byte
|
||||||
|
|
||||||
|
func (o *objectID) UnmarshalParam(param string) error {
|
||||||
|
oid, err := convertTo(param)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*o = oid
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertTo(s string) (objectID, error) {
|
||||||
|
var nilObjectID objectID
|
||||||
|
if len(s) != 24 {
|
||||||
|
return nilObjectID, fmt.Errorf("invalid format")
|
||||||
|
}
|
||||||
|
|
||||||
|
var oid [12]byte
|
||||||
|
_, err := hex.Decode(oid[:], []byte(s))
|
||||||
|
if err != nil {
|
||||||
|
return nilObjectID, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return oid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomArrayUri(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
FileData objectID `uri:"id"`
|
||||||
|
}
|
||||||
|
val := `664a062ac74a8ad104e0e80f`
|
||||||
|
err := mappingByPtr(&s, formSource{"id": {val}}, "uri")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
expected, _ := convertTo(val)
|
||||||
|
assert.EqualValues(t, expected, s.FileData)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMappingCustomArrayForm(t *testing.T) {
|
||||||
|
var s struct {
|
||||||
|
FileData objectID `form:"id"`
|
||||||
|
}
|
||||||
|
val := `664a062ac74a8ad104e0e80f`
|
||||||
|
err := mappingByPtr(&s, formSource{"id": {val}}, "form")
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
expected, _ := convertTo(val)
|
||||||
|
assert.EqualValues(t, expected, s.FileData)
|
||||||
|
}
|
||||||
|
@ -475,7 +475,7 @@ func (c *Context) QueryArray(key string) (values []string) {
|
|||||||
|
|
||||||
func (c *Context) initQueryCache() {
|
func (c *Context) initQueryCache() {
|
||||||
if c.queryCache == nil {
|
if c.queryCache == nil {
|
||||||
if c.Request != nil {
|
if c.Request != nil && c.Request.URL != nil {
|
||||||
c.queryCache = c.Request.URL.Query()
|
c.queryCache = c.Request.URL.Query()
|
||||||
} else {
|
} else {
|
||||||
c.queryCache = url.Values{}
|
c.queryCache = url.Values{}
|
||||||
|
@ -423,6 +423,49 @@ func TestContextQuery(t *testing.T) {
|
|||||||
assert.Empty(t, c.PostForm("foo"))
|
assert.Empty(t, c.PostForm("foo"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContextInitQueryCache(t *testing.T) {
|
||||||
|
validURL, err := url.Parse("https://github.com/gin-gonic/gin/pull/3969?key=value&otherkey=othervalue")
|
||||||
|
assert.Nil(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
testName string
|
||||||
|
testContext *Context
|
||||||
|
expectedQueryCache url.Values
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
testName: "queryCache should remain unchanged if already not nil",
|
||||||
|
testContext: &Context{
|
||||||
|
queryCache: url.Values{"a": []string{"b"}},
|
||||||
|
Request: &http.Request{URL: validURL}, // valid request for evidence that values weren't extracted
|
||||||
|
},
|
||||||
|
expectedQueryCache: url.Values{"a": []string{"b"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "queryCache should be empty when Request is nil",
|
||||||
|
testContext: &Context{Request: nil}, // explicit nil for readability
|
||||||
|
expectedQueryCache: url.Values{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "queryCache should be empty when Request.URL is nil",
|
||||||
|
testContext: &Context{Request: &http.Request{URL: nil}}, // explicit nil for readability
|
||||||
|
expectedQueryCache: url.Values{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
testName: "queryCache should be populated when it not yet populated and Request + Request.URL are non nil",
|
||||||
|
testContext: &Context{Request: &http.Request{URL: validURL}}, // explicit nil for readability
|
||||||
|
expectedQueryCache: url.Values{"key": []string{"value"}, "otherkey": []string{"othervalue"}},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.testName, func(t *testing.T) {
|
||||||
|
test.testContext.initQueryCache()
|
||||||
|
assert.Equal(t, test.expectedQueryCache, test.testContext.queryCache)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
func TestContextDefaultQueryOnEmptyRequest(t *testing.T) {
|
func TestContextDefaultQueryOnEmptyRequest(t *testing.T) {
|
||||||
c, _ := CreateTestContext(httptest.NewRecorder()) // here c.Request == nil
|
c, _ := CreateTestContext(httptest.NewRecorder()) // here c.Request == nil
|
||||||
assert.NotPanics(t, func() {
|
assert.NotPanics(t, func() {
|
||||||
|
25
gin.go
25
gin.go
@ -24,6 +24,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const defaultMultipartMemory = 32 << 20 // 32 MB
|
const defaultMultipartMemory = 32 << 20 // 32 MB
|
||||||
|
const escapedColon = "\\:"
|
||||||
|
const colon = ":"
|
||||||
|
const backslash = "\\"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
default404Body = []byte("404 page not found")
|
default404Body = []byte("404 page not found")
|
||||||
@ -474,6 +477,26 @@ func (engine *Engine) validateHeader(header string) (clientIP string, valid bool
|
|||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// updateRouteTree do update to the route tree recursively
|
||||||
|
func updateRouteTree(n *node) {
|
||||||
|
n.path = strings.ReplaceAll(n.path, escapedColon, colon)
|
||||||
|
n.fullPath = strings.ReplaceAll(n.fullPath, escapedColon, colon)
|
||||||
|
n.indices = strings.ReplaceAll(n.indices, backslash, colon)
|
||||||
|
if n.children == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, child := range n.children {
|
||||||
|
updateRouteTree(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// updateRouteTrees do update to the route trees
|
||||||
|
func (engine *Engine) updateRouteTrees() {
|
||||||
|
for _, tree := range engine.trees {
|
||||||
|
updateRouteTree(tree.root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// parseIP parse a string representation of an IP and returns a net.IP with the
|
// parseIP parse a string representation of an IP and returns a net.IP with the
|
||||||
// minimum byte representation or nil if input is invalid.
|
// minimum byte representation or nil if input is invalid.
|
||||||
func parseIP(ip string) net.IP {
|
func parseIP(ip string) net.IP {
|
||||||
@ -498,7 +521,7 @@ func (engine *Engine) Run(addr ...string) (err error) {
|
|||||||
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
|
debugPrint("[WARNING] You trusted all proxies, this is NOT safe. We recommend you to set a value.\n" +
|
||||||
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
|
"Please check https://github.com/gin-gonic/gin/blob/master/docs/doc.md#dont-trust-all-proxies for details.")
|
||||||
}
|
}
|
||||||
|
engine.updateRouteTrees()
|
||||||
address := resolveAddress(addr)
|
address := resolveAddress(addr)
|
||||||
debugPrint("Listening and serving HTTP on %s\n", address)
|
debugPrint("Listening and serving HTTP on %s\n", address)
|
||||||
err = http.ListenAndServe(address, engine.Handler())
|
err = http.ListenAndServe(address, engine.Handler())
|
||||||
|
@ -577,3 +577,28 @@ func TestTreeRunDynamicRouting(t *testing.T) {
|
|||||||
func isWindows() bool {
|
func isWindows() bool {
|
||||||
return runtime.GOOS == "windows"
|
return runtime.GOOS == "windows"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestEscapedColon(t *testing.T) {
|
||||||
|
router := New()
|
||||||
|
f := func(u string) {
|
||||||
|
router.GET(u, func(c *Context) { c.String(http.StatusOK, u) })
|
||||||
|
}
|
||||||
|
f("/r/r\\:r")
|
||||||
|
f("/r/r:r")
|
||||||
|
f("/r/r/:r")
|
||||||
|
f("/r/r/\\:r")
|
||||||
|
f("/r/r/r\\:r")
|
||||||
|
assert.Panics(t, func() {
|
||||||
|
f("\\foo:")
|
||||||
|
})
|
||||||
|
|
||||||
|
router.updateRouteTrees()
|
||||||
|
ts := httptest.NewServer(router)
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
testRequest(t, ts.URL+"/r/r123", "", "/r/r:r")
|
||||||
|
testRequest(t, ts.URL+"/r/r:r", "", "/r/r\\:r")
|
||||||
|
testRequest(t, ts.URL+"/r/r/r123", "", "/r/r/:r")
|
||||||
|
testRequest(t, ts.URL+"/r/r/:r", "", "/r/r/\\:r")
|
||||||
|
testRequest(t, ts.URL+"/r/r/r:r", "", "/r/r/r\\:r")
|
||||||
|
}
|
||||||
|
27
tree.go
27
tree.go
@ -65,17 +65,10 @@ func (trees methodTrees) get(method string) *node {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func min(a, b int) int {
|
|
||||||
if a <= b {
|
|
||||||
return a
|
|
||||||
}
|
|
||||||
return b
|
|
||||||
}
|
|
||||||
|
|
||||||
func longestCommonPrefix(a, b string) int {
|
func longestCommonPrefix(a, b string) int {
|
||||||
i := 0
|
i := 0
|
||||||
max := min(len(a), len(b))
|
max_ := min(len(a), len(b))
|
||||||
for i < max && a[i] == b[i] {
|
for i < max_ && a[i] == b[i] {
|
||||||
i++
|
i++
|
||||||
}
|
}
|
||||||
return i
|
return i
|
||||||
@ -205,7 +198,7 @@ walk:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if a child with the next path byte exists
|
// Check if a child with the next path byte exists
|
||||||
for i, max := 0, len(n.indices); i < max; i++ {
|
for i, max_ := 0, len(n.indices); i < max_; i++ {
|
||||||
if c == n.indices[i] {
|
if c == n.indices[i] {
|
||||||
parentFullPathIndex += len(n.path)
|
parentFullPathIndex += len(n.path)
|
||||||
i = n.incrementChildPrio(i)
|
i = n.incrementChildPrio(i)
|
||||||
@ -269,7 +262,19 @@ walk:
|
|||||||
// Returns -1 as index, if no wildcard was found.
|
// Returns -1 as index, if no wildcard was found.
|
||||||
func findWildcard(path string) (wildcard string, i int, valid bool) {
|
func findWildcard(path string) (wildcard string, i int, valid bool) {
|
||||||
// Find start
|
// Find start
|
||||||
|
escapeColon := false
|
||||||
for start, c := range []byte(path) {
|
for start, c := range []byte(path) {
|
||||||
|
if escapeColon {
|
||||||
|
escapeColon = false
|
||||||
|
if c == ':' {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
panic("invalid escape string in path '" + path + "'")
|
||||||
|
}
|
||||||
|
if c == '\\' {
|
||||||
|
escapeColon = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
// A wildcard starts with ':' (param) or '*' (catch-all)
|
// A wildcard starts with ':' (param) or '*' (catch-all)
|
||||||
if c != ':' && c != '*' {
|
if c != ':' && c != '*' {
|
||||||
continue
|
continue
|
||||||
@ -770,7 +775,7 @@ walk: // Outer loop for walking the tree
|
|||||||
// Runes are up to 4 byte long,
|
// Runes are up to 4 byte long,
|
||||||
// -4 would definitely be another rune.
|
// -4 would definitely be another rune.
|
||||||
var off int
|
var off int
|
||||||
for max := min(npLen, 3); off < max; off++ {
|
for max_ := min(npLen, 3); off < max_; off++ {
|
||||||
if i := npLen - off; utf8.RuneStart(oldPath[i]) {
|
if i := npLen - off; utf8.RuneStart(oldPath[i]) {
|
||||||
// read rune from cached path
|
// read rune from cached path
|
||||||
rv, _ = utf8.DecodeRuneInString(oldPath[i:])
|
rv, _ = utf8.DecodeRuneInString(oldPath[i:])
|
||||||
|
22
tree_test.go
22
tree_test.go
@ -192,6 +192,7 @@ func TestTreeWildcard(t *testing.T) {
|
|||||||
"/get/abc/123abg/:param",
|
"/get/abc/123abg/:param",
|
||||||
"/get/abc/123abf/:param",
|
"/get/abc/123abf/:param",
|
||||||
"/get/abc/123abfff/:param",
|
"/get/abc/123abfff/:param",
|
||||||
|
"/get/abc/escaped_colon/test\\:param",
|
||||||
}
|
}
|
||||||
for _, route := range routes {
|
for _, route := range routes {
|
||||||
tree.addRoute(route, fakeHandler(route))
|
tree.addRoute(route, fakeHandler(route))
|
||||||
@ -315,6 +316,7 @@ func TestTreeWildcard(t *testing.T) {
|
|||||||
{"/get/abc/123abg/test", false, "/get/abc/123abg/:param", Params{Param{Key: "param", Value: "test"}}},
|
{"/get/abc/123abg/test", false, "/get/abc/123abg/:param", Params{Param{Key: "param", Value: "test"}}},
|
||||||
{"/get/abc/123abf/testss", false, "/get/abc/123abf/:param", Params{Param{Key: "param", Value: "testss"}}},
|
{"/get/abc/123abf/testss", false, "/get/abc/123abf/:param", Params{Param{Key: "param", Value: "testss"}}},
|
||||||
{"/get/abc/123abfff/te", false, "/get/abc/123abfff/:param", Params{Param{Key: "param", Value: "te"}}},
|
{"/get/abc/123abfff/te", false, "/get/abc/123abfff/:param", Params{Param{Key: "param", Value: "te"}}},
|
||||||
|
{"/get/abc/escaped_colon/test\\:param", false, "/get/abc/escaped_colon/test\\:param", nil},
|
||||||
})
|
})
|
||||||
|
|
||||||
checkPriorities(t, tree)
|
checkPriorities(t, tree)
|
||||||
@ -419,6 +421,9 @@ func TestTreeWildcardConflict(t *testing.T) {
|
|||||||
{"/id/:id", false},
|
{"/id/:id", false},
|
||||||
{"/static/*file", false},
|
{"/static/*file", false},
|
||||||
{"/static/", true},
|
{"/static/", true},
|
||||||
|
{"/escape/test\\:d1", false},
|
||||||
|
{"/escape/test\\:d2", false},
|
||||||
|
{"/escape/test:param", false},
|
||||||
}
|
}
|
||||||
testRoutes(t, routes)
|
testRoutes(t, routes)
|
||||||
}
|
}
|
||||||
@ -971,3 +976,20 @@ func TestTreeWildcardConflictEx(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTreeInvalidEscape(t *testing.T) {
|
||||||
|
routes := map[string]bool{
|
||||||
|
"/r1/r": true,
|
||||||
|
"/r2/:r": true,
|
||||||
|
"/r3/\\:r": true,
|
||||||
|
}
|
||||||
|
tree := &node{}
|
||||||
|
for route, valid := range routes {
|
||||||
|
recv := catchPanic(func() {
|
||||||
|
tree.addRoute(route, fakeHandler(route))
|
||||||
|
})
|
||||||
|
if recv == nil != valid {
|
||||||
|
t.Fatalf("%s should be %t but got %v", route, valid, recv)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user