mirror of https://github.com/gofiber/fiber.git
🚀 new possibility to escape special routing parameters (#1280)
* 🚀 new possibility to escape special routing parameters, which gives the possibility to follow the google api design guide https://cloud.google.com/apis/design/custom_methods * 🚀 new possibility to escape special routing parameters, which gives the possibility to follow the google api design guide https://cloud.google.com/apis/design/custom_methodspull/1282/head
parent
a3600f0ff9
commit
033184938c
53
path.go
53
path.go
|
@ -41,11 +41,12 @@ type routeSegment struct {
|
||||||
|
|
||||||
// different special routing signs
|
// different special routing signs
|
||||||
const (
|
const (
|
||||||
wildcardParam byte = '*' // indicates a optional greedy parameter
|
wildcardParam byte = '*' // indicates a optional greedy parameter
|
||||||
plusParam byte = '+' // indicates a required greedy parameter
|
plusParam byte = '+' // indicates a required greedy parameter
|
||||||
optionalParam byte = '?' // concludes a parameter by name and makes it optional
|
optionalParam byte = '?' // concludes a parameter by name and makes it optional
|
||||||
paramStarterChar byte = ':' // start character for a parameter with name
|
paramStarterChar byte = ':' // start character for a parameter with name
|
||||||
slashDelimiter byte = '/' // separator for the route, unlike the other delimiters this character at the end can be optional
|
slashDelimiter byte = '/' // separator for the route, unlike the other delimiters this character at the end can be optional
|
||||||
|
escapeChar byte = '\\' // escape character
|
||||||
)
|
)
|
||||||
|
|
||||||
// list of possible parameter and segment delimiter
|
// list of possible parameter and segment delimiter
|
||||||
|
@ -102,7 +103,7 @@ func addParameterMetaInfo(segs []*routeSegment) []*routeSegment {
|
||||||
// set the compare part for the parameter
|
// set the compare part for the parameter
|
||||||
if segs[i].IsParam {
|
if segs[i].IsParam {
|
||||||
// important for finding the end of the parameter
|
// important for finding the end of the parameter
|
||||||
segs[i].ComparePart = comparePart
|
segs[i].ComparePart = RemoveEscapeChar(comparePart)
|
||||||
} else {
|
} else {
|
||||||
comparePart = segs[i].Const
|
comparePart = segs[i].Const
|
||||||
if len(comparePart) > 1 {
|
if len(comparePart) > 1 {
|
||||||
|
@ -140,11 +141,11 @@ func addParameterMetaInfo(segs []*routeSegment) []*routeSegment {
|
||||||
|
|
||||||
// findNextParamPosition search for the next possible parameter start position
|
// findNextParamPosition search for the next possible parameter start position
|
||||||
func findNextParamPosition(pattern string) int {
|
func findNextParamPosition(pattern string) int {
|
||||||
nextParamPosition := findNextCharsetPosition(pattern, parameterStartChars)
|
nextParamPosition := findNextNonEscapedCharsetPosition(pattern, parameterStartChars)
|
||||||
if nextParamPosition != -1 && len(pattern) > nextParamPosition && pattern[nextParamPosition] != wildcardParam {
|
if nextParamPosition != -1 && len(pattern) > nextParamPosition && pattern[nextParamPosition] != wildcardParam {
|
||||||
// search for parameter characters for the found parameter start,
|
// search for parameter characters for the found parameter start,
|
||||||
// if there are more, move the parameter start to the last parameter char
|
// if there are more, move the parameter start to the last parameter char
|
||||||
for found := findNextCharsetPosition(pattern[nextParamPosition+1:], parameterStartChars); found == 0; {
|
for found := findNextNonEscapedCharsetPosition(pattern[nextParamPosition+1:], parameterStartChars); found == 0; {
|
||||||
nextParamPosition++
|
nextParamPosition++
|
||||||
if len(pattern) > nextParamPosition {
|
if len(pattern) > nextParamPosition {
|
||||||
break
|
break
|
||||||
|
@ -163,9 +164,10 @@ func (routeParser *routeParser) analyseConstantPart(pattern string, nextParamPos
|
||||||
// remove the constant part until the parameter
|
// remove the constant part until the parameter
|
||||||
processedPart = pattern[:nextParamPosition]
|
processedPart = pattern[:nextParamPosition]
|
||||||
}
|
}
|
||||||
|
constPart := RemoveEscapeChar(processedPart)
|
||||||
return processedPart, &routeSegment{
|
return processedPart, &routeSegment{
|
||||||
Const: processedPart,
|
Const: constPart,
|
||||||
Length: len(processedPart),
|
Length: len(constPart),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -173,7 +175,7 @@ func (routeParser *routeParser) analyseConstantPart(pattern string, nextParamPos
|
||||||
func (routeParser *routeParser) analyseParameterPart(pattern string) (string, *routeSegment) {
|
func (routeParser *routeParser) analyseParameterPart(pattern string) (string, *routeSegment) {
|
||||||
isWildCard := pattern[0] == wildcardParam
|
isWildCard := pattern[0] == wildcardParam
|
||||||
isPlusParam := pattern[0] == plusParam
|
isPlusParam := pattern[0] == plusParam
|
||||||
parameterEndPosition := findNextCharsetPosition(pattern[1:], parameterEndChars)
|
parameterEndPosition := findNextNonEscapedCharsetPosition(pattern[1:], parameterEndChars)
|
||||||
|
|
||||||
// handle wildcard end
|
// handle wildcard end
|
||||||
if isWildCard || isPlusParam {
|
if isWildCard || isPlusParam {
|
||||||
|
@ -186,7 +188,7 @@ func (routeParser *routeParser) analyseParameterPart(pattern string) (string, *r
|
||||||
// cut params part
|
// cut params part
|
||||||
processedPart := pattern[0 : parameterEndPosition+1]
|
processedPart := pattern[0 : parameterEndPosition+1]
|
||||||
|
|
||||||
paramName := GetTrimmedParam(processedPart)
|
paramName := RemoveEscapeChar(GetTrimmedParam(processedPart))
|
||||||
// add access iterator to wildcard and plus
|
// add access iterator to wildcard and plus
|
||||||
if isWildCard {
|
if isWildCard {
|
||||||
routeParser.wildCardCount++
|
routeParser.wildCardCount++
|
||||||
|
@ -226,6 +228,25 @@ func findNextCharsetPosition(search string, charset []byte) int {
|
||||||
return nextPosition
|
return nextPosition
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// findNextNonEscapedCharsetPosition search the next char position from the charset and skip the escaped characters
|
||||||
|
func findNextNonEscapedCharsetPosition(search string, charset []byte) int {
|
||||||
|
pos := findNextCharsetPosition(search, charset)
|
||||||
|
for pos > 0 && search[pos-1] == escapeChar {
|
||||||
|
if len(search) == pos+1 {
|
||||||
|
// escaped character is at the end
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
nextPossiblePos := findNextCharsetPosition(search[pos+1:], charset)
|
||||||
|
if nextPossiblePos == -1 {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
// the previous character is taken into consideration
|
||||||
|
pos = nextPossiblePos + pos + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
return pos
|
||||||
|
}
|
||||||
|
|
||||||
// getMatch parses the passed url and tries to match it against the route segments and determine the parameter positions
|
// getMatch parses the passed url and tries to match it against the route segments and determine the parameter positions
|
||||||
func (routeParser *routeParser) getMatch(detectionPath, path string, params *[maxParams]string, partialCheck bool) bool {
|
func (routeParser *routeParser) getMatch(detectionPath, path string, params *[maxParams]string, partialCheck bool) bool {
|
||||||
var i, paramsIterator, partLen int
|
var i, paramsIterator, partLen int
|
||||||
|
@ -339,3 +360,11 @@ func GetTrimmedParam(param string) string {
|
||||||
|
|
||||||
return param[start:end]
|
return param[start:end]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RemoveEscapeChar remove escape characters
|
||||||
|
func RemoveEscapeChar(word string) string {
|
||||||
|
if strings.IndexByte(word, escapeChar) != -1 {
|
||||||
|
return strings.ReplaceAll(word, string(escapeChar), "")
|
||||||
|
}
|
||||||
|
return word
|
||||||
|
}
|
||||||
|
|
40
path_test.go
40
path_test.go
|
@ -40,6 +40,26 @@ func Test_Path_parseRoute(t *testing.T) {
|
||||||
wildCardCount: 1,
|
wildCardCount: 1,
|
||||||
}, rp)
|
}, rp)
|
||||||
|
|
||||||
|
rp = parseRoute("/v1/some/resource/name\\:customVerb")
|
||||||
|
utils.AssertEqual(t, routeParser{
|
||||||
|
segs: []*routeSegment{
|
||||||
|
{Const: "/v1/some/resource/name:customVerb", Length: 33, IsLast: true},
|
||||||
|
},
|
||||||
|
params: nil,
|
||||||
|
}, rp)
|
||||||
|
// heavy test with escaped charaters
|
||||||
|
rp = parseRoute("/v1/some/resource/name\\\\:customVerb?\\?/:param/*")
|
||||||
|
utils.AssertEqual(t, routeParser{
|
||||||
|
segs: []*routeSegment{
|
||||||
|
{Const: "/v1/some/resource/name:customVerb??/", Length: 36},
|
||||||
|
{IsParam: true, ParamName: "param", ComparePart: "/", PartCount: 1},
|
||||||
|
{Const: "/", Length: 1, HasOptionalSlash: true},
|
||||||
|
{IsParam: true, ParamName: "*1", IsGreedy: true, IsOptional: true, IsLast: true},
|
||||||
|
},
|
||||||
|
params: []string{"param", "*1"},
|
||||||
|
wildCardCount: 1,
|
||||||
|
}, rp)
|
||||||
|
|
||||||
rp = parseRoute("/api/*/:param/:param2")
|
rp = parseRoute("/api/*/:param/:param2")
|
||||||
utils.AssertEqual(t, routeParser{
|
utils.AssertEqual(t, routeParser{
|
||||||
segs: []*routeSegment{
|
segs: []*routeSegment{
|
||||||
|
@ -118,7 +138,7 @@ func Test_Path_matchParams(t *testing.T) {
|
||||||
match := parser.getMatch(c.url, c.url, &ctxParams, c.partialCheck)
|
match := parser.getMatch(c.url, c.url, &ctxParams, c.partialCheck)
|
||||||
utils.AssertEqual(t, c.match, match, fmt.Sprintf("route: '%s', url: '%s'", r, c.url))
|
utils.AssertEqual(t, c.match, match, fmt.Sprintf("route: '%s', url: '%s'", r, c.url))
|
||||||
if match && len(c.params) > 0 {
|
if match && len(c.params) > 0 {
|
||||||
utils.AssertEqual(t, c.params[0:len(c.params)-1], ctxParams[0:len(c.params)-1], fmt.Sprintf("route: '%s', url: '%s'", r, c.url))
|
utils.AssertEqual(t, c.params[0:len(c.params)], ctxParams[0:len(c.params)], fmt.Sprintf("route: '%s', url: '%s'", r, c.url))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -146,6 +166,14 @@ func Test_Path_matchParams(t *testing.T) {
|
||||||
{url: "/api/v2", params: nil, match: false},
|
{url: "/api/v2", params: nil, match: false},
|
||||||
{url: "/api/xyz", params: nil, match: false},
|
{url: "/api/xyz", params: nil, match: false},
|
||||||
})
|
})
|
||||||
|
testCase("/v1/some/resource/name\\:customVerb", []testparams{
|
||||||
|
{url: "/v1/some/resource/name:customVerb", params: nil, match: true},
|
||||||
|
{url: "/v1/some/resource/name:test", params: nil, match: false},
|
||||||
|
})
|
||||||
|
testCase("/v1/some/resource/name\\\\:customVerb?\\?/:param/*", []testparams{
|
||||||
|
{url: "/v1/some/resource/name:customVerb??/test/optionalWildCard/character", params: []string{"test", "optionalWildCard/character"}, match: true},
|
||||||
|
{url: "/v1/some/resource/name:customVerb??/test", params: []string{"test", ""}, match: true},
|
||||||
|
})
|
||||||
testCase("/api/v1/*", []testparams{
|
testCase("/api/v1/*", []testparams{
|
||||||
{url: "/api/v1", params: []string{""}, match: true},
|
{url: "/api/v1", params: []string{""}, match: true},
|
||||||
{url: "/api/v1/", params: []string{""}, match: true},
|
{url: "/api/v1/", params: []string{""}, match: true},
|
||||||
|
@ -402,6 +430,16 @@ func Test_Utils_GetTrimmedParam(t *testing.T) {
|
||||||
utils.AssertEqual(t, "noParam", res)
|
utils.AssertEqual(t, "noParam", res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func Test_Utils_RemoveEscapeChar(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
res := RemoveEscapeChar(":test\\:bla")
|
||||||
|
utils.AssertEqual(t, ":test:bla", res)
|
||||||
|
res = RemoveEscapeChar("\\abc")
|
||||||
|
utils.AssertEqual(t, "abc", res)
|
||||||
|
res = RemoveEscapeChar("noEscapeChar")
|
||||||
|
utils.AssertEqual(t, "noEscapeChar", res)
|
||||||
|
}
|
||||||
|
|
||||||
// go test -race -run Test_Path_matchParams
|
// go test -race -run Test_Path_matchParams
|
||||||
func Benchmark_Path_matchParams(t *testing.B) {
|
func Benchmark_Path_matchParams(t *testing.B) {
|
||||||
type testparams struct {
|
type testparams struct {
|
||||||
|
|
Loading…
Reference in New Issue