From 033184938cd87f0221e0ff54387586505c800dee Mon Sep 17 00:00:00 2001 From: RW Date: Mon, 12 Apr 2021 21:45:24 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=80=20new=20possibility=20to=20escape?= =?UTF-8?q?=20special=20routing=20parameters=20(#1280)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🚀 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_methods --- path.go | 53 ++++++++++++++++++++++++++++++++++++++++------------ path_test.go | 40 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 13 deletions(-) diff --git a/path.go b/path.go index 3a5fdd46..02dc0119 100644 --- a/path.go +++ b/path.go @@ -41,11 +41,12 @@ type routeSegment struct { // different special routing signs const ( - wildcardParam byte = '*' // indicates a optional greedy parameter - plusParam byte = '+' // indicates a required greedy parameter - optionalParam byte = '?' // concludes a parameter by name and makes it optional - 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 + wildcardParam byte = '*' // indicates a optional greedy parameter + plusParam byte = '+' // indicates a required greedy parameter + optionalParam byte = '?' // concludes a parameter by name and makes it optional + 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 + escapeChar byte = '\\' // escape character ) // list of possible parameter and segment delimiter @@ -102,7 +103,7 @@ func addParameterMetaInfo(segs []*routeSegment) []*routeSegment { // set the compare part for the parameter if segs[i].IsParam { // important for finding the end of the parameter - segs[i].ComparePart = comparePart + segs[i].ComparePart = RemoveEscapeChar(comparePart) } else { comparePart = segs[i].Const if len(comparePart) > 1 { @@ -140,11 +141,11 @@ func addParameterMetaInfo(segs []*routeSegment) []*routeSegment { // findNextParamPosition search for the next possible parameter start position func findNextParamPosition(pattern string) int { - nextParamPosition := findNextCharsetPosition(pattern, parameterStartChars) + nextParamPosition := findNextNonEscapedCharsetPosition(pattern, parameterStartChars) if nextParamPosition != -1 && len(pattern) > nextParamPosition && pattern[nextParamPosition] != wildcardParam { // search for parameter characters for the found parameter start, // 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++ if len(pattern) > nextParamPosition { break @@ -163,9 +164,10 @@ func (routeParser *routeParser) analyseConstantPart(pattern string, nextParamPos // remove the constant part until the parameter processedPart = pattern[:nextParamPosition] } + constPart := RemoveEscapeChar(processedPart) return processedPart, &routeSegment{ - Const: processedPart, - Length: len(processedPart), + Const: constPart, + Length: len(constPart), } } @@ -173,7 +175,7 @@ func (routeParser *routeParser) analyseConstantPart(pattern string, nextParamPos func (routeParser *routeParser) analyseParameterPart(pattern string) (string, *routeSegment) { isWildCard := pattern[0] == wildcardParam isPlusParam := pattern[0] == plusParam - parameterEndPosition := findNextCharsetPosition(pattern[1:], parameterEndChars) + parameterEndPosition := findNextNonEscapedCharsetPosition(pattern[1:], parameterEndChars) // handle wildcard end if isWildCard || isPlusParam { @@ -186,7 +188,7 @@ func (routeParser *routeParser) analyseParameterPart(pattern string) (string, *r // cut params part processedPart := pattern[0 : parameterEndPosition+1] - paramName := GetTrimmedParam(processedPart) + paramName := RemoveEscapeChar(GetTrimmedParam(processedPart)) // add access iterator to wildcard and plus if isWildCard { routeParser.wildCardCount++ @@ -226,6 +228,25 @@ func findNextCharsetPosition(search string, charset []byte) int { 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 func (routeParser *routeParser) getMatch(detectionPath, path string, params *[maxParams]string, partialCheck bool) bool { var i, paramsIterator, partLen int @@ -339,3 +360,11 @@ func GetTrimmedParam(param string) string { 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 +} diff --git a/path_test.go b/path_test.go index 8cc6cc05..3b492936 100644 --- a/path_test.go +++ b/path_test.go @@ -40,6 +40,26 @@ func Test_Path_parseRoute(t *testing.T) { wildCardCount: 1, }, 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") utils.AssertEqual(t, routeParser{ segs: []*routeSegment{ @@ -118,7 +138,7 @@ func Test_Path_matchParams(t *testing.T) { 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)) 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/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{ {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) } +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 func Benchmark_Path_matchParams(t *testing.B) { type testparams struct {