diff --git a/.travis.yml b/.travis.yml index d7086b38..91129313 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,6 @@ language: go matrix: fast_finish: true include: - - go: 1.11.x - env: GO111MODULE=on - go: 1.12.x env: GO111MODULE=on - go: 1.13.x @@ -20,6 +18,34 @@ matrix: env: - TESTTAGS=nomsgpack - go: master + # Adding ppc64le jobs + - go: 1.11.x + arch: ppc64le + env: GO111MODULE=on + - go: 1.12.x + arch: ppc64le + env: GO111MODULE=on + - go: 1.13.x + arch: ppc64le + - go: 1.13.x + arch: ppc64le + env: + - TESTTAGS=nomsgpack + - go: 1.14.x + arch: ppc64le + - go: 1.14.x + arch: ppc64le + env: + - TESTTAGS=nomsgpack + - go: 1.15.x + arch: ppc64le + - go: 1.15.x + arch: ppc64le + env: + - TESTTAGS=nomsgpack + - go: master + arch: ppc64le + git: depth: 10 diff --git a/README.md b/README.md index 74725ee0..4aa1f497 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi To install Gin package, you need to install Go and set your Go workspace first. -1. The first need [Go](https://golang.org/) installed (**version 1.11+ is required**), then you can use the below Go command to install Gin. +1. The first need [Go](https://golang.org/) installed (**version 1.12+ is required**), then you can use the below Go command to install Gin. ```sh $ go get -u github.com/gin-gonic/gin @@ -178,8 +178,8 @@ Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httpr - [x] Zero allocation router. - [x] Still the fastest http router and framework. From routing to writing. -- [x] Complete suite of unit tests -- [x] Battle tested +- [x] Complete suite of unit tests. +- [x] Battle tested. - [x] API frozen, new releases will not break your code. ## Build with [jsoniter](https://github.com/json-iterator/go) @@ -340,7 +340,7 @@ func main() { ``` ``` -ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou] +ids: map[b:hello a:1234]; names: map[second:tianou first:thinkerou] ``` ### Upload files @@ -1255,6 +1255,7 @@ func main() { } reader := response.Body + defer reader.Close() contentLength := response.ContentLength contentType := response.Header.Get("Content-Type") diff --git a/binding/binding.go b/binding/binding.go index 57562845..5c8e235b 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -51,7 +51,8 @@ type BindingUri interface { // https://github.com/go-playground/validator/tree/v8.18.2. type StructValidator interface { // ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right. - // If the received type is not a struct, any validation should be skipped and nil must be returned. + // If the received type is a slice|array, the validation should be performed travel on every element. + // If the received type is not a struct or slice|array, any validation should be skipped and nil must be returned. // If the received type is a struct or pointer to a struct, the validation should be performed. // If the struct is not valid or the validation itself fails, a descriptive error should be returned. // Otherwise nil must be returned. diff --git a/binding/binding_test.go b/binding/binding_test.go index 4424bab9..17336177 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -13,6 +13,7 @@ import ( "mime/multipart" "net/http" "os" + "reflect" "strconv" "strings" "testing" @@ -34,7 +35,7 @@ type QueryTest struct { } type FooStruct struct { - Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required"` + Foo string `msgpack:"foo" json:"foo" form:"foo" xml:"foo" binding:"required,max=32"` } type FooBarStruct struct { @@ -180,6 +181,20 @@ func TestBindingJSON(t *testing.T) { `{"foo": "bar"}`, `{"bar": "foo"}`) } +func TestBindingJSONSlice(t *testing.T) { + EnableDecoderDisallowUnknownFields = true + defer func() { + EnableDecoderDisallowUnknownFields = false + }() + + testBodyBindingSlice(t, JSON, "json", "/", "/", `[]`, ``) + testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{}]`) + testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": ""}]`) + testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": 123}]`) + testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"bar": 123}]`) + testBodyBindingSlice(t, JSON, "json", "/", "/", `[{"foo": "123"}]`, `[{"foo": "123456789012345678901234567890123"}]`) +} + func TestBindingJSONUseNumber(t *testing.T) { testBodyBindingUseNumber(t, JSON, "json", @@ -200,6 +215,12 @@ func TestBindingJSONDisallowUnknownFields(t *testing.T) { `{"foo": "bar"}`, `{"foo": "bar", "what": "this"}`) } +func TestBindingJSONStringMap(t *testing.T) { + testBodyBindingStringMap(t, JSON, + "/", "/", + `{"foo": "bar", "hello": "world"}`, `{"num": 2}`) +} + func TestBindingForm(t *testing.T) { testFormBinding(t, "POST", "/", "/", @@ -336,6 +357,37 @@ func TestBindingFormForType(t *testing.T) { "", "", "StructPointer") } +func TestBindingFormStringMap(t *testing.T) { + testBodyBindingStringMap(t, Form, + "/", "", + `foo=bar&hello=world`, "") + // Should pick the last value + testBodyBindingStringMap(t, Form, + "/", "", + `foo=something&foo=bar&hello=world`, "") +} + +func TestBindingFormStringSliceMap(t *testing.T) { + obj := make(map[string][]string) + req := requestWithBody("POST", "/", "foo=something&foo=bar&hello=world") + req.Header.Add("Content-Type", MIMEPOSTForm) + err := Form.Bind(req, &obj) + assert.NoError(t, err) + assert.NotNil(t, obj) + assert.Len(t, obj, 2) + target := map[string][]string{ + "foo": {"something", "bar"}, + "hello": {"world"}, + } + assert.True(t, reflect.DeepEqual(obj, target)) + + objInvalid := make(map[string][]int) + req = requestWithBody("POST", "/", "foo=something&foo=bar&hello=world") + req.Header.Add("Content-Type", MIMEPOSTForm) + err = Form.Bind(req, &objInvalid) + assert.Error(t, err) +} + func TestBindingQuery(t *testing.T) { testQueryBinding(t, "POST", "/?foo=bar&bar=foo", "/", @@ -366,6 +418,28 @@ func TestBindingQueryBoolFail(t *testing.T) { "bool_foo=unused", "") } +func TestBindingQueryStringMap(t *testing.T) { + b := Query + + obj := make(map[string]string) + req := requestWithBody("GET", "/?foo=bar&hello=world", "") + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.NotNil(t, obj) + assert.Len(t, obj, 2) + assert.Equal(t, "bar", obj["foo"]) + assert.Equal(t, "world", obj["hello"]) + + obj = make(map[string]string) + req = requestWithBody("GET", "/?foo=bar&foo=2&hello=world", "") // should pick last + err = b.Bind(req, &obj) + assert.NoError(t, err) + assert.NotNil(t, obj) + assert.Len(t, obj, 2) + assert.Equal(t, "2", obj["foo"]) + assert.Equal(t, "world", obj["hello"]) +} + func TestBindingXML(t *testing.T) { testBodyBinding(t, XML, "xml", @@ -387,6 +461,13 @@ func TestBindingYAML(t *testing.T) { `foo: bar`, `bar: foo`) } +func TestBindingYAMLStringMap(t *testing.T) { + // YAML is a superset of JSON, so the test below is JSON (to avoid newlines) + testBodyBindingStringMap(t, YAML, + "/", "/", + `{"foo": "bar", "hello": "world"}`, `{"nested": {"foo": "bar"}}`) +} + func TestBindingYAMLFail(t *testing.T) { testBodyBindingFail(t, YAML, "yaml", @@ -1114,6 +1195,46 @@ func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody assert.Error(t, err) } +func testBodyBindingSlice(t *testing.T, b Binding, name, path, badPath, body, badBody string) { + assert.Equal(t, name, b.Name()) + + var obj1 []FooStruct + req := requestWithBody("POST", path, body) + err := b.Bind(req, &obj1) + assert.NoError(t, err) + + var obj2 []FooStruct + req = requestWithBody("POST", badPath, badBody) + err = JSON.Bind(req, &obj2) + assert.Error(t, err) +} + +func testBodyBindingStringMap(t *testing.T, b Binding, path, badPath, body, badBody string) { + obj := make(map[string]string) + req := requestWithBody("POST", path, body) + if b.Name() == "form" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.NotNil(t, obj) + assert.Len(t, obj, 2) + assert.Equal(t, "bar", obj["foo"]) + assert.Equal(t, "world", obj["hello"]) + + if badPath != "" && badBody != "" { + obj = make(map[string]string) + req = requestWithBody("POST", badPath, badBody) + err = b.Bind(req, &obj) + assert.Error(t, err) + } + + objInt := make(map[string]int) + req = requestWithBody("POST", path, body) + err = b.Bind(req, &objInt) + assert.Error(t, err) +} + func testBodyBindingUseNumber(t *testing.T, b Binding, name, path, badPath, body, badBody string) { assert.Equal(t, name, b.Name()) diff --git a/binding/default_validator.go b/binding/default_validator.go index a4c1a7f6..c57a120f 100644 --- a/binding/default_validator.go +++ b/binding/default_validator.go @@ -5,7 +5,9 @@ package binding import ( + "fmt" "reflect" + "strings" "sync" "github.com/go-playground/validator/v10" @@ -16,22 +18,54 @@ type defaultValidator struct { validate *validator.Validate } +type sliceValidateError []error + +func (err sliceValidateError) Error() string { + var errMsgs []string + for i, e := range err { + if e == nil { + continue + } + errMsgs = append(errMsgs, fmt.Sprintf("[%d]: %s", i, e.Error())) + } + return strings.Join(errMsgs, "\n") +} + var _ StructValidator = &defaultValidator{} // ValidateStruct receives any kind of type, but only performed struct or pointer to struct type. func (v *defaultValidator) ValidateStruct(obj interface{}) error { + if obj == nil { + return nil + } + value := reflect.ValueOf(obj) - valueType := value.Kind() - if valueType == reflect.Ptr { - valueType = value.Elem().Kind() - } - if valueType == reflect.Struct { - v.lazyinit() - if err := v.validate.Struct(obj); err != nil { - return err + switch value.Kind() { + case reflect.Ptr: + return v.ValidateStruct(value.Elem().Interface()) + case reflect.Struct: + return v.validateStruct(obj) + case reflect.Slice, reflect.Array: + count := value.Len() + validateRet := make(sliceValidateError, 0) + for i := 0; i < count; i++ { + if err := v.ValidateStruct(value.Index(i).Interface()); err != nil { + validateRet = append(validateRet, err) + } } + if len(validateRet) == 0 { + return nil + } + return validateRet + default: + return nil } - return nil +} + +// validateStruct receives struct type +func (v *defaultValidator) validateStruct(obj interface{}) error { + v.lazyinit() + return v.validate.Struct(obj) } // Engine returns the underlying validator engine which powers the default diff --git a/binding/default_validator_test.go b/binding/default_validator_test.go new file mode 100644 index 00000000..e9c6de44 --- /dev/null +++ b/binding/default_validator_test.go @@ -0,0 +1,68 @@ +// Copyright 2020 Gin Core Team. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "errors" + "testing" +) + +func TestSliceValidateError(t *testing.T) { + tests := []struct { + name string + err sliceValidateError + want string + }{ + {"has nil elements", sliceValidateError{errors.New("test error"), nil}, "[0]: test error"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := tt.err.Error(); got != tt.want { + t.Errorf("sliceValidateError.Error() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestDefaultValidator(t *testing.T) { + type exampleStruct struct { + A string `binding:"max=8"` + B int `binding:"gt=0"` + } + tests := []struct { + name string + v *defaultValidator + obj interface{} + wantErr bool + }{ + {"validate nil obj", &defaultValidator{}, nil, false}, + {"validate int obj", &defaultValidator{}, 3, false}, + {"validate struct failed-1", &defaultValidator{}, exampleStruct{A: "123456789", B: 1}, true}, + {"validate struct failed-2", &defaultValidator{}, exampleStruct{A: "12345678", B: 0}, true}, + {"validate struct passed", &defaultValidator{}, exampleStruct{A: "12345678", B: 1}, false}, + {"validate *struct failed-1", &defaultValidator{}, &exampleStruct{A: "123456789", B: 1}, true}, + {"validate *struct failed-2", &defaultValidator{}, &exampleStruct{A: "12345678", B: 0}, true}, + {"validate *struct passed", &defaultValidator{}, &exampleStruct{A: "12345678", B: 1}, false}, + {"validate []struct failed-1", &defaultValidator{}, []exampleStruct{{A: "123456789", B: 1}}, true}, + {"validate []struct failed-2", &defaultValidator{}, []exampleStruct{{A: "12345678", B: 0}}, true}, + {"validate []struct passed", &defaultValidator{}, []exampleStruct{{A: "12345678", B: 1}}, false}, + {"validate []*struct failed-1", &defaultValidator{}, []*exampleStruct{{A: "123456789", B: 1}}, true}, + {"validate []*struct failed-2", &defaultValidator{}, []*exampleStruct{{A: "12345678", B: 0}}, true}, + {"validate []*struct passed", &defaultValidator{}, []*exampleStruct{{A: "12345678", B: 1}}, false}, + {"validate *[]struct failed-1", &defaultValidator{}, &[]exampleStruct{{A: "123456789", B: 1}}, true}, + {"validate *[]struct failed-2", &defaultValidator{}, &[]exampleStruct{{A: "12345678", B: 0}}, true}, + {"validate *[]struct passed", &defaultValidator{}, &[]exampleStruct{{A: "12345678", B: 1}}, false}, + {"validate *[]*struct failed-1", &defaultValidator{}, &[]*exampleStruct{{A: "123456789", B: 1}}, true}, + {"validate *[]*struct failed-2", &defaultValidator{}, &[]*exampleStruct{{A: "12345678", B: 0}}, true}, + {"validate *[]*struct passed", &defaultValidator{}, &[]*exampleStruct{{A: "12345678", B: 1}}, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := tt.v.ValidateStruct(tt.obj); (err != nil) != tt.wantErr { + t.Errorf("defaultValidator.Validate() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/binding/form_mapping.go b/binding/form_mapping.go index f0913ea5..2f4e45b4 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -29,6 +29,21 @@ func mapForm(ptr interface{}, form map[string][]string) error { var emptyField = reflect.StructField{} func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error { + // Check if ptr is a map + ptrVal := reflect.ValueOf(ptr) + var pointed interface{} + if ptrVal.Kind() == reflect.Ptr { + ptrVal = ptrVal.Elem() + pointed = ptrVal.Interface() + } + if ptrVal.Kind() == reflect.Map && + ptrVal.Type().Key().Kind() == reflect.String { + if pointed != nil { + ptr = pointed + } + return setFormMap(ptr, form) + } + return mappingByPtr(ptr, formSource(form), tag) } @@ -349,3 +364,29 @@ func head(str, sep string) (head string, tail string) { } return str[:idx], str[idx+len(sep):] } + +func setFormMap(ptr interface{}, form map[string][]string) error { + el := reflect.TypeOf(ptr).Elem() + + if el.Kind() == reflect.Slice { + ptrMap, ok := ptr.(map[string][]string) + if !ok { + return errors.New("cannot convert to map slices of strings") + } + for k, v := range form { + ptrMap[k] = v + } + + return nil + } + + ptrMap, ok := ptr.(map[string]string) + if !ok { + return errors.New("cannot convert to map of strings") + } + for k, v := range form { + ptrMap[k] = v[len(v)-1] // pick last + } + + return nil +} diff --git a/binding/json_test.go b/binding/json_test.go index cae4cccc..fbd5c527 100644 --- a/binding/json_test.go +++ b/binding/json_test.go @@ -19,3 +19,12 @@ func TestJSONBindingBindBody(t *testing.T) { require.NoError(t, err) assert.Equal(t, "FOO", s.Foo) } + +func TestJSONBindingBindBodyMap(t *testing.T) { + s := make(map[string]string) + err := jsonBinding{}.BindBody([]byte(`{"foo": "FOO","hello":"world"}`), &s) + require.NoError(t, err) + assert.Len(t, s, 2) + assert.Equal(t, "FOO", s["foo"]) + assert.Equal(t, "world", s["hello"]) +} diff --git a/context.go b/context.go index 2b33c207..9e7a6578 100644 --- a/context.go +++ b/context.go @@ -295,6 +295,22 @@ func (c *Context) GetInt64(key string) (i64 int64) { return } +// GetUint returns the value associated with the key as an unsigned integer. +func (c *Context) GetUint(key string) (ui uint) { + if val, ok := c.Get(key); ok && val != nil { + ui, _ = val.(uint) + } + return +} + +// GetUint64 returns the value associated with the key as an unsigned integer. +func (c *Context) GetUint64(key string) (ui64 uint64) { + if val, ok := c.Get(key); ok && val != nil { + ui64, _ = val.(uint64) + } + return +} + // GetFloat64 returns the value associated with the key as a float64. func (c *Context) GetFloat64(key string) (f64 float64) { if val, ok := c.Get(key); ok && val != nil { @@ -944,7 +960,7 @@ func (c *Context) SecureJSON(code int, obj interface{}) { } // JSONP serializes the given struct as JSON into the response body. -// It add padding to response body to request data from a server residing in a different domain than the client. +// It adds padding to response body to request data from a server residing in a different domain than the client. // It also sets the Content-Type as "application/javascript". func (c *Context) JSONP(code int, obj interface{}) { callback := c.DefaultQuery("callback", "") @@ -1021,12 +1037,12 @@ func (c *Context) DataFromReader(code int, contentLength int64, contentType stri }) } -// File writes the specified file into the body stream in a efficient way. +// File writes the specified file into the body stream in an efficient way. func (c *Context) File(filepath string) { http.ServeFile(c.Writer, c.Request, filepath) } -// FileFromFS writes the specified file from http.FileSytem into the body stream in an efficient way. +// FileFromFS writes the specified file from http.FileSystem into the body stream in an efficient way. func (c *Context) FileFromFS(filepath string, fs http.FileSystem) { defer func(old string) { c.Request.URL.Path = old @@ -1040,7 +1056,7 @@ func (c *Context) FileFromFS(filepath string, fs http.FileSystem) { // FileAttachment writes the specified file into the body stream in an efficient way // On the client side, the file will typically be downloaded with the given filename func (c *Context) FileAttachment(filepath, filename string) { - c.Writer.Header().Set("content-disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) + c.Writer.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filename)) http.ServeFile(c.Writer, c.Request, filepath) } diff --git a/context_test.go b/context_test.go index 7ffc3aaf..16c4dd28 100644 --- a/context_test.go +++ b/context_test.go @@ -261,6 +261,18 @@ func TestContextGetInt64(t *testing.T) { assert.Equal(t, int64(42424242424242), c.GetInt64("int64")) } +func TestContextGetUint(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Set("uint", uint(1)) + assert.Equal(t, uint(1), c.GetUint("uint")) +} + +func TestContextGetUint64(t *testing.T) { + c, _ := CreateTestContext(httptest.NewRecorder()) + c.Set("uint64", uint64(18446744073709551615)) + assert.Equal(t, uint64(18446744073709551615), c.GetUint64("uint64")) +} + func TestContextGetFloat64(t *testing.T) { c, _ := CreateTestContext(httptest.NewRecorder()) c.Set("float64", 4.2) @@ -1270,7 +1282,7 @@ func TestContextIsAborted(t *testing.T) { assert.True(t, c.IsAborted()) } -// TestContextData tests that the response can be written from `bytesting` +// TestContextData tests that the response can be written from `bytestring` // with specified MIME type func TestContextAbortWithStatus(t *testing.T) { w := httptest.NewRecorder() diff --git a/errors.go b/errors.go index 9a317992..0f276c13 100644 --- a/errors.go +++ b/errors.go @@ -90,6 +90,11 @@ func (msg *Error) IsType(flags ErrorType) bool { return (msg.Type & flags) > 0 } +// Unwrap returns the wrapped error, to allow interoperability with errors.Is(), errors.As() and errors.Unwrap() +func (msg *Error) Unwrap() error { + return msg.Err +} + // ByType returns a readonly copy filtered the byte. // ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic. func (a errorMsgs) ByType(typ ErrorType) errorMsgs { diff --git a/errors_1.13_test.go b/errors_1.13_test.go new file mode 100644 index 00000000..a8f9a94e --- /dev/null +++ b/errors_1.13_test.go @@ -0,0 +1,33 @@ +// +build go1.13 + +package gin + +import ( + "errors" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +type TestErr string + +func (e TestErr) Error() string { return string(e) } + +// TestErrorUnwrap tests the behavior of gin.Error with "errors.Is()" and "errors.As()". +// "errors.Is()" and "errors.As()" have been added to the standard library in go 1.13, +// hence the "// +build go1.13" directive at the beginning of this file. +func TestErrorUnwrap(t *testing.T) { + innerErr := TestErr("somme error") + + // 2 layers of wrapping : use 'fmt.Errorf("%w")' to wrap a gin.Error{}, which itself wraps innerErr + err := fmt.Errorf("wrapped: %w", &Error{ + Err: innerErr, + Type: ErrorTypeAny, + }) + + // check that 'errors.Is()' and 'errors.As()' behave as expected : + assert.True(t, errors.Is(err, innerErr)) + var testErr TestErr + assert.True(t, errors.As(err, &testErr)) +} diff --git a/go.mod b/go.mod index cfaee746..884ff851 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.13 require ( github.com/gin-contrib/sse v0.1.0 - github.com/go-playground/validator/v10 v10.2.0 + github.com/go-playground/validator/v10 v10.4.1 github.com/golang/protobuf v1.3.3 github.com/json-iterator/go v1.1.9 github.com/mattn/go-isatty v0.0.12 diff --git a/go.sum b/go.sum index 4c14fb83..a64b3319 100644 --- a/go.sum +++ b/go.sum @@ -9,8 +9,8 @@ github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8c github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= -github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE= +github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4= github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -34,8 +34,15 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/internal/bytesconv/bytesconv.go b/internal/bytesconv/bytesconv.go index 7b80e335..fdad2015 100644 --- a/internal/bytesconv/bytesconv.go +++ b/internal/bytesconv/bytesconv.go @@ -5,16 +5,17 @@ package bytesconv import ( - "reflect" "unsafe" ) // StringToBytes converts string to byte slice without a memory allocation. func StringToBytes(s string) (b []byte) { - sh := *(*reflect.StringHeader)(unsafe.Pointer(&s)) - bh := (*reflect.SliceHeader)(unsafe.Pointer(&b)) - bh.Data, bh.Len, bh.Cap = sh.Data, sh.Len, sh.Len - return b + return *(*[]byte)(unsafe.Pointer( + &struct { + string + Cap int + }{s, len(s)}, + )) } // BytesToString converts byte slice to string without a memory allocation. diff --git a/recovery.go b/recovery.go index d02b829b..563f5aaa 100644 --- a/recovery.go +++ b/recovery.go @@ -76,11 +76,12 @@ func CustomRecoveryWithWriter(out io.Writer, handle RecoveryFunc) HandlerFunc { headers[idx] = current[0] + ": *" } } + headersToStr := strings.Join(headers, "\r\n") if brokenPipe { - logger.Printf("%s\n%s%s", err, string(httpRequest), reset) + logger.Printf("%s\n%s%s", err, headersToStr, reset) } else if IsDebugging() { logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", - timeFormat(time.Now()), strings.Join(headers, "\r\n"), err, stack, reset) + timeFormat(time.Now()), headersToStr, err, stack, reset) } else { logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", timeFormat(time.Now()), err, stack, reset) diff --git a/render/text.go b/render/text.go index 30f5f532..461b720a 100644 --- a/render/text.go +++ b/render/text.go @@ -7,6 +7,8 @@ package render import ( "fmt" "net/http" + + "github.com/gin-gonic/gin/internal/bytesconv" ) // String contains the given interface object slice and its format. @@ -34,6 +36,6 @@ func WriteString(w http.ResponseWriter, format string, data []interface{}) (err _, err = fmt.Fprintf(w, format, data...) return } - _, err = w.Write([]byte(format)) + _, err = w.Write(bytesconv.StringToBytes(format)) return } diff --git a/tree.go b/tree.go index 7a80af9e..74e07e84 100644 --- a/tree.go +++ b/tree.go @@ -119,7 +119,6 @@ func (n *node) incrementChildPrio(pos int) int { for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- { // Swap node positions cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1] - } // Build new index char string @@ -559,8 +558,8 @@ func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) ([]by // Use a static sized buffer on the stack in the common case. // If the path is too long, allocate a buffer on the heap instead. buf := make([]byte, 0, stackBufSize) - if l := len(path) + 1; l > stackBufSize { - buf = make([]byte, 0, l) + if length := len(path) + 1; length > stackBufSize { + buf = make([]byte, 0, length) } ciPath := n.findCaseInsensitivePathRec( @@ -600,142 +599,7 @@ walk: // Outer loop for walking the tree path = path[npLen:] ciPath = append(ciPath, n.path...) - if len(path) > 0 { - // If this node does not have a wildcard (param or catchAll) child, - // we can just look up the next child node and continue to walk down - // the tree - if !n.wildChild { - // Skip rune bytes already processed - rb = shiftNRuneBytes(rb, npLen) - - if rb[0] != 0 { - // Old rune not finished - idxc := rb[0] - for i, c := range []byte(n.indices) { - if c == idxc { - // continue with child node - n = n.children[i] - npLen = len(n.path) - continue walk - } - } - } else { - // Process a new rune - var rv rune - - // Find rune start. - // Runes are up to 4 byte long, - // -4 would definitely be another rune. - var off int - for max := min(npLen, 3); off < max; off++ { - if i := npLen - off; utf8.RuneStart(oldPath[i]) { - // read rune from cached path - rv, _ = utf8.DecodeRuneInString(oldPath[i:]) - break - } - } - - // Calculate lowercase bytes of current rune - lo := unicode.ToLower(rv) - utf8.EncodeRune(rb[:], lo) - - // Skip already processed bytes - rb = shiftNRuneBytes(rb, off) - - idxc := rb[0] - for i, c := range []byte(n.indices) { - // Lowercase matches - if c == idxc { - // must use a recursive approach since both the - // uppercase byte and the lowercase byte might exist - // as an index - if out := n.children[i].findCaseInsensitivePathRec( - path, ciPath, rb, fixTrailingSlash, - ); out != nil { - return out - } - break - } - } - - // If we found no match, the same for the uppercase rune, - // if it differs - if up := unicode.ToUpper(rv); up != lo { - utf8.EncodeRune(rb[:], up) - rb = shiftNRuneBytes(rb, off) - - idxc := rb[0] - for i, c := range []byte(n.indices) { - // Uppercase matches - if c == idxc { - // Continue with child node - n = n.children[i] - npLen = len(n.path) - continue walk - } - } - } - } - - // Nothing found. We can recommend to redirect to the same URL - // without a trailing slash if a leaf exists for that path - if fixTrailingSlash && path == "/" && n.handlers != nil { - return ciPath - } - return nil - } - - n = n.children[0] - switch n.nType { - case param: - // Find param end (either '/' or path end) - end := 0 - for end < len(path) && path[end] != '/' { - end++ - } - - // Add param value to case insensitive path - ciPath = append(ciPath, path[:end]...) - - // We need to go deeper! - if end < len(path) { - if len(n.children) > 0 { - // Continue with child node - n = n.children[0] - npLen = len(n.path) - path = path[end:] - continue - } - - // ... but we can't - if fixTrailingSlash && len(path) == end+1 { - return ciPath - } - return nil - } - - if n.handlers != nil { - return ciPath - } - - if fixTrailingSlash && len(n.children) == 1 { - // No handle found. Check if a handle for this path + a - // trailing slash exists - n = n.children[0] - if n.path == "/" && n.handlers != nil { - return append(ciPath, '/') - } - } - - return nil - - case catchAll: - return append(ciPath, path...) - - default: - panic("invalid node type") - } - } else { + if len(path) == 0 { // We should have reached the node containing the handle. // Check if this node has a handle registered. if n.handlers != nil { @@ -758,6 +622,141 @@ walk: // Outer loop for walking the tree } return nil } + + // If this node does not have a wildcard (param or catchAll) child, + // we can just look up the next child node and continue to walk down + // the tree + if !n.wildChild { + // Skip rune bytes already processed + rb = shiftNRuneBytes(rb, npLen) + + if rb[0] != 0 { + // Old rune not finished + idxc := rb[0] + for i, c := range []byte(n.indices) { + if c == idxc { + // continue with child node + n = n.children[i] + npLen = len(n.path) + continue walk + } + } + } else { + // Process a new rune + var rv rune + + // Find rune start. + // Runes are up to 4 byte long, + // -4 would definitely be another rune. + var off int + for max := min(npLen, 3); off < max; off++ { + if i := npLen - off; utf8.RuneStart(oldPath[i]) { + // read rune from cached path + rv, _ = utf8.DecodeRuneInString(oldPath[i:]) + break + } + } + + // Calculate lowercase bytes of current rune + lo := unicode.ToLower(rv) + utf8.EncodeRune(rb[:], lo) + + // Skip already processed bytes + rb = shiftNRuneBytes(rb, off) + + idxc := rb[0] + for i, c := range []byte(n.indices) { + // Lowercase matches + if c == idxc { + // must use a recursive approach since both the + // uppercase byte and the lowercase byte might exist + // as an index + if out := n.children[i].findCaseInsensitivePathRec( + path, ciPath, rb, fixTrailingSlash, + ); out != nil { + return out + } + break + } + } + + // If we found no match, the same for the uppercase rune, + // if it differs + if up := unicode.ToUpper(rv); up != lo { + utf8.EncodeRune(rb[:], up) + rb = shiftNRuneBytes(rb, off) + + idxc := rb[0] + for i, c := range []byte(n.indices) { + // Uppercase matches + if c == idxc { + // Continue with child node + n = n.children[i] + npLen = len(n.path) + continue walk + } + } + } + } + + // Nothing found. We can recommend to redirect to the same URL + // without a trailing slash if a leaf exists for that path + if fixTrailingSlash && path == "/" && n.handlers != nil { + return ciPath + } + return nil + } + + n = n.children[0] + switch n.nType { + case param: + // Find param end (either '/' or path end) + end := 0 + for end < len(path) && path[end] != '/' { + end++ + } + + // Add param value to case insensitive path + ciPath = append(ciPath, path[:end]...) + + // We need to go deeper! + if end < len(path) { + if len(n.children) > 0 { + // Continue with child node + n = n.children[0] + npLen = len(n.path) + path = path[end:] + continue + } + + // ... but we can't + if fixTrailingSlash && len(path) == end+1 { + return ciPath + } + return nil + } + + if n.handlers != nil { + return ciPath + } + + if fixTrailingSlash && len(n.children) == 1 { + // No handle found. Check if a handle for this path + a + // trailing slash exists + n = n.children[0] + if n.path == "/" && n.handlers != nil { + return append(ciPath, '/') + } + } + + return nil + + case catchAll: + return append(ciPath, path...) + + default: + panic("invalid node type") + } } // Nothing found. diff --git a/utils.go b/utils.go index fab3aee3..c32f0eeb 100644 --- a/utils.go +++ b/utils.go @@ -103,7 +103,10 @@ func parseAccept(acceptHeader string) []string { parts := strings.Split(acceptHeader, ",") out := make([]string, 0, len(parts)) for _, part := range parts { - if part = strings.TrimSpace(strings.Split(part, ";")[0]); part != "" { + if i := strings.IndexByte(part, ';'); i > 0 { + part = part[:i] + } + if part = strings.TrimSpace(part); part != "" { out = append(out, part) } } diff --git a/utils_test.go b/utils_test.go index 9b57c57b..cc486c35 100644 --- a/utils_test.go +++ b/utils_test.go @@ -18,6 +18,12 @@ func init() { SetMode(TestMode) } +func BenchmarkParseAccept(b *testing.B) { + for i := 0; i < b.N; i++ { + parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8") + } +} + type testStruct struct { T *testing.T }