Merge branch 'gin-gonic:master' into master

This commit is contained in:
Leon cap 2025-07-29 10:47:12 +08:00 committed by GitHub
commit c57a166902
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 302 additions and 31 deletions

View File

@ -29,3 +29,8 @@ jobs:
args: release --clean args: release --clean
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Trigger Go module reindex (pkg.go.dev)
run: |
echo "Triggering Go module reindex at proxy.golang.org"
curl -sSf "https://proxy.golang.org/github.com/${GITHUB_REPOSITORY,,}/@v/${GITHUB_REF_NAME}.info"

View File

@ -175,7 +175,7 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter
// BindUnmarshaler is the interface used to wrap the UnmarshalParam method. // BindUnmarshaler is the interface used to wrap the UnmarshalParam method.
type BindUnmarshaler interface { type BindUnmarshaler interface {
// UnmarshalParam decodes and assigns a value from an form or query param. // UnmarshalParam decodes and assigns a value from a form or query param.
UnmarshalParam(param string) error UnmarshalParam(param string) error
} }

View File

@ -216,7 +216,7 @@ func (c *Context) AbortWithStatus(code int) {
c.Abort() c.Abort()
} }
// AbortWithStatusJSON calls `Abort()` and then `PureJSON` internally. // AbortWithStatusPureJSON calls `Abort()` and then `PureJSON` internally.
// This method stops the chain, writes the status code and return a JSON body without escaping. // This method stops the chain, writes the status code and return a JSON body without escaping.
// It also sets the Content-Type as "application/json". // It also sets the Content-Type as "application/json".
func (c *Context) AbortWithStatusPureJSON(code int, jsonObj any) { func (c *Context) AbortWithStatusPureJSON(code int, jsonObj any) {
@ -573,7 +573,7 @@ func (c *Context) QueryMap(key string) (dicts map[string]string) {
// whether at least one value exists for the given key. // whether at least one value exists for the given key.
func (c *Context) GetQueryMap(key string) (map[string]string, bool) { func (c *Context) GetQueryMap(key string) (map[string]string, bool) {
c.initQueryCache() c.initQueryCache()
return c.get(c.queryCache, key) return getMapFromFormData(c.queryCache, key)
} }
// PostForm returns the specified key from a POST urlencoded form or multipart form // PostForm returns the specified key from a POST urlencoded form or multipart form
@ -646,22 +646,23 @@ func (c *Context) PostFormMap(key string) (dicts map[string]string) {
// whether at least one value exists for the given key. // whether at least one value exists for the given key.
func (c *Context) GetPostFormMap(key string) (map[string]string, bool) { func (c *Context) GetPostFormMap(key string) (map[string]string, bool) {
c.initFormCache() c.initFormCache()
return c.get(c.formCache, key) return getMapFromFormData(c.formCache, key)
} }
// get is an internal method and returns a map which satisfies conditions. // getMapFromFormData return a map which satisfies conditions.
func (c *Context) get(m map[string][]string, key string) (map[string]string, bool) { // It parses from data with bracket notation like "key[subkey]=value" into a map.
dicts := make(map[string]string) func getMapFromFormData(m map[string][]string, key string) (map[string]string, bool) {
exist := false d := make(map[string]string)
found := false
for k, v := range m { for k, v := range m {
if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key { if i := strings.IndexByte(k, '['); i >= 1 && k[0:i] == key {
if j := strings.IndexByte(k[i+1:], ']'); j >= 1 { if j := strings.IndexByte(k[i+1:], ']'); j >= 1 {
exist = true found = true
dicts[k[i+1:][:j]] = v[0] d[k[i+1:][:j]] = v[0]
} }
} }
} }
return dicts, exist return d, found
} }
// FormFile returns the first file for the provided form key. // FormFile returns the first file for the provided form key.

35
context_file_test.go Normal file
View File

@ -0,0 +1,35 @@
package gin
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
// TestContextFileSimple tests the Context.File() method with a simple case
func TestContextFileSimple(t *testing.T) {
// Test serving an existing file
testFile := "testdata/test_file.txt"
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
c.File(testFile)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "This is a test file")
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
}
// TestContextFileNotFound tests serving a non-existent file
func TestContextFileNotFound(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
c.File("non_existent_file.txt")
assert.Equal(t, http.StatusNotFound, w.Code)
}

View File

@ -76,6 +76,79 @@ func must(err error) {
} }
} }
// TestContextFile tests the Context.File() method
func TestContextFile(t *testing.T) {
// Test serving an existing file
t.Run("serve existing file", func(t *testing.T) {
// Create a temporary test file
testFile := "testdata/test_file.txt"
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
c.File(testFile)
assert.Equal(t, http.StatusOK, w.Code)
assert.Contains(t, w.Body.String(), "This is a test file")
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
})
// Test serving a non-existent file
t.Run("serve non-existent file", func(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
c.File("non_existent_file.txt")
assert.Equal(t, http.StatusNotFound, w.Code)
})
// Test serving a directory (should return 200 with directory listing or 403 Forbidden)
t.Run("serve directory", func(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
c.File(".")
// Directory serving can return either 200 (with listing) or 403 (forbidden)
assert.True(t, w.Code == http.StatusOK || w.Code == http.StatusForbidden)
})
// Test with HEAD request
t.Run("HEAD request", func(t *testing.T) {
testFile := "testdata/test_file.txt"
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodHead, "/test", nil)
c.File(testFile)
assert.Equal(t, http.StatusOK, w.Code)
assert.Empty(t, w.Body.String()) // HEAD request should not return body
assert.Equal(t, "text/plain; charset=utf-8", w.Header().Get("Content-Type"))
})
// Test with Range request
t.Run("Range request", func(t *testing.T) {
testFile := "testdata/test_file.txt"
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodGet, "/test", nil)
c.Request.Header.Set("Range", "bytes=0-10")
c.File(testFile)
assert.Equal(t, http.StatusPartialContent, w.Code)
assert.Equal(t, "bytes", w.Header().Get("Accept-Ranges"))
assert.Contains(t, w.Header().Get("Content-Range"), "bytes 0-10")
})
}
func TestContextFormFile(t *testing.T) { func TestContextFormFile(t *testing.T) {
buf := new(bytes.Buffer) buf := new(bytes.Buffer)
mw := multipart.NewWriter(buf) mw := multipart.NewWriter(buf)
@ -3470,3 +3543,134 @@ func TestContextSetCookieData(t *testing.T) {
assert.Contains(t, setCookie, "SameSite=Strict") assert.Contains(t, setCookie, "SameSite=Strict")
}) })
} }
func TestGetMapFromFormData(t *testing.T) {
testCases := []struct {
name string
data map[string][]string
key string
expected map[string]string
found bool
}{
{
name: "Basic bracket notation",
data: map[string][]string{
"ids[a]": {"hi"},
"ids[b]": {"3.14"},
},
key: "ids",
expected: map[string]string{
"a": "hi",
"b": "3.14",
},
found: true,
},
{
name: "Mixed data with bracket notation",
data: map[string][]string{
"ids[a]": {"hi"},
"ids[b]": {"3.14"},
"names[a]": {"mike"},
"names[b]": {"maria"},
"other[key]": {"value"},
"simple": {"data"},
},
key: "ids",
expected: map[string]string{
"a": "hi",
"b": "3.14",
},
found: true,
},
{
name: "Names key",
data: map[string][]string{
"ids[a]": {"hi"},
"ids[b]": {"3.14"},
"names[a]": {"mike"},
"names[b]": {"maria"},
"other[key]": {"value"},
},
key: "names",
expected: map[string]string{
"a": "mike",
"b": "maria",
},
found: true,
},
{
name: "Key not found",
data: map[string][]string{
"ids[a]": {"hi"},
"names[b]": {"maria"},
},
key: "notfound",
expected: map[string]string{},
found: false,
},
{
name: "Empty data",
data: map[string][]string{},
key: "ids",
expected: map[string]string{},
found: false,
},
{
name: "Malformed bracket notation",
data: map[string][]string{
"ids[a": {"hi"}, // Missing closing bracket
"ids]b": {"3.14"}, // Missing opening bracket
"idsab": {"value"}, // No brackets
},
key: "ids",
expected: map[string]string{},
found: false,
},
{
name: "Nested bracket notation",
data: map[string][]string{
"ids[a][b]": {"nested"},
"ids[c]": {"simple"},
},
key: "ids",
expected: map[string]string{
"a": "nested",
"c": "simple",
},
found: true,
},
{
name: "Simple key without brackets",
data: map[string][]string{
"simple": {"data"},
"ids[a]": {"hi"},
},
key: "simple",
expected: map[string]string{},
found: false,
},
{
name: "Mixed simple and bracket keys",
data: map[string][]string{
"simple": {"data"},
"ids[a]": {"hi"},
"ids[b]": {"3.14"},
"other": {"value"},
},
key: "ids",
expected: map[string]string{
"a": "hi",
"b": "3.14",
},
found: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result, found := getMapFromFormData(tc.data, tc.key)
assert.Equal(t, tc.expected, result, "result mismatch")
assert.Equal(t, tc.found, found, "found mismatch")
})
}
}

12
go.mod
View File

@ -15,7 +15,7 @@ require (
github.com/quic-go/quic-go v0.53.0 github.com/quic-go/quic-go v0.53.0
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
github.com/ugorji/go/codec v1.3.0 github.com/ugorji/go/codec v1.3.0
golang.org/x/net v0.41.0 golang.org/x/net v0.42.0
google.golang.org/protobuf v1.36.6 google.golang.org/protobuf v1.36.6
) )
@ -34,11 +34,11 @@ require (
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
go.uber.org/mock v0.5.0 // indirect go.uber.org/mock v0.5.0 // indirect
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect golang.org/x/arch v0.0.0-20210923205945-b76863e36670 // indirect
golang.org/x/crypto v0.39.0 // indirect golang.org/x/crypto v0.40.0 // indirect
golang.org/x/mod v0.25.0 // indirect golang.org/x/mod v0.25.0 // indirect
golang.org/x/sync v0.15.0 // indirect golang.org/x/sync v0.16.0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.34.0 // indirect
golang.org/x/text v0.26.0 // indirect golang.org/x/text v0.27.0 // indirect
golang.org/x/tools v0.33.0 // indirect golang.org/x/tools v0.34.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

24
go.sum
View File

@ -67,21 +67,21 @@ go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU= golang.org/x/arch v0.0.0-20210923205945-b76863e36670 h1:18EFjUmQOcUvxNYSkA6jO9VAiXCnxFY6NyDX0bHDmkU=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= golang.org/x/crypto v0.40.0 h1:r4x+VvoG5Fm+eJcxMaY8CQM7Lb0l1lsmjGBQ6s8BfKM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= golang.org/x/crypto v0.40.0/go.mod h1:Qr1vMER5WyS2dfPHAlsOj01wgLbsyWtFn/aY+5+ZdxY=
golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8=
golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

View File

@ -44,7 +44,7 @@ type LoggerConfig struct {
// Optional. Default value is gin.DefaultWriter. // Optional. Default value is gin.DefaultWriter.
Output io.Writer Output io.Writer
// SkipPaths is an url path array which logs are not written. // SkipPaths is a URL path array which logs are not written.
// Optional. // Optional.
SkipPaths []string SkipPaths []string

View File

@ -65,7 +65,7 @@ func SetMode(value string) {
} }
switch value { switch value {
case DebugMode, "": case DebugMode:
atomic.StoreInt32(&ginMode, debugCode) atomic.StoreInt32(&ginMode, debugCode)
case ReleaseMode: case ReleaseMode:
atomic.StoreInt32(&ginMode, releaseCode) atomic.StoreInt32(&ginMode, releaseCode)

View File

@ -11,6 +11,8 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
var MaxHandlers = 32
func init() { func init() {
SetMode(TestMode) SetMode(TestMode)
} }
@ -193,3 +195,25 @@ func testRoutesInterface(t *testing.T, r IRoutes) {
assert.Equal(t, r, r.Static("/static", ".")) assert.Equal(t, r, r.Static("/static", "."))
assert.Equal(t, r, r.StaticFS("/static2", Dir(".", false))) assert.Equal(t, r, r.StaticFS("/static2", Dir(".", false)))
} }
func TestRouterGroupCombineHandlersTooManyHandlers(t *testing.T) {
group := &RouterGroup{
Handlers: make(HandlersChain, MaxHandlers), // Assume group already has MaxHandlers middleware
}
tooManyHandlers := make(HandlersChain, MaxHandlers) // Add MaxHandlers more, total 2 * MaxHandlers
// This should trigger panic
assert.Panics(t, func() {
group.combineHandlers(tooManyHandlers)
}, "should panic due to too many handlers")
}
func TestRouterGroupCombineHandlersEmptySliceNotNil(t *testing.T) {
group := &RouterGroup{
Handlers: HandlersChain{},
}
result := group.combineHandlers(HandlersChain{})
assert.NotNil(t, result, "result should not be nil even with empty handlers")
assert.Empty(t, result, "empty handlers should return empty chain")
}

2
testdata/test_file.txt vendored Normal file
View File

@ -0,0 +1,2 @@
This is a test file for Context.File() method testing.
It contains some sample content to verify file serving functionality.