1
0
mirror of https://github.com/labstack/echo.git synced 2024-12-24 20:14:31 +02:00

Poc router stack backtracking (#1791)

Router: PoC stack based backtracking

Co-authored-by: stffabi <stffabi@users.noreply.github.com>
This commit is contained in:
Martti T 2021-03-02 20:56:40 +02:00 committed by GitHub
parent b2444d8399
commit 6f9b71cd6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 243 additions and 127 deletions

194
router.go
View File

@ -2,7 +2,6 @@ package echo
import (
"net/http"
"strings"
)
type (
@ -334,21 +333,48 @@ func (r *Router) Find(method, path string, c Context) {
cn := r.tree // Current node as root
var (
search = path
child *node // Child node
n int // Param counter
nk kind // Next kind
nn *node // Next node
ns string // Next search
pvalues = ctx.pvalues // Use the internal slice so the interface can keep the illusion of a dynamic slice
search = path
searchIndex = 0
n int // Param counter
pvalues = ctx.pvalues // Use the internal slice so the interface can keep the illusion of a dynamic slice
)
// Backtracking is needed when a dead end (leaf node) is reached in the router tree.
// To backtrack the current node will be changed to the parent node and the next kind for the
// router logic will be returned based on fromKind or kind of the dead end node (static > param > any).
// For example if there is no static node match we should check parent next sibling by kind (param).
// Backtracking itself does not check if there is a next sibling, this is done by the router logic.
backtrackToNextNodeKind := func(fromKind kind) (nextNodeKind kind, valid bool) {
previous := cn
cn = previous.parent
valid = cn != nil
// Next node type by priority
// NOTE: With the current implementation we never backtrack from an `any` route, so `previous.kind` is
// always `static` or `any`
// If this is changed then for any route next kind would be `static` and this statement should be changed
nextNodeKind = previous.kind + 1
if fromKind == skind {
// when backtracking is done from static kind block we did not change search so nothing to restore
return
}
// restore search to value it was before we move to current node we are backtracking from.
if previous.kind == skind {
searchIndex -= len(previous.prefix)
} else {
n--
// for param/any node.prefix value is always `:` so we can not deduce searchIndex from that and must use pValue
// for that index as it would also contain part of path we cut off before moving into node we are backtracking from
searchIndex -= len(pvalues[n])
}
search = path[searchIndex:]
return
}
// Search order static > param > any
for {
if search == "" {
break
}
pl := 0 // Prefix length
l := 0 // LCP length
@ -365,60 +391,42 @@ func (r *Router) Find(method, path string, c Context) {
}
}
if l == pl {
// Continue search
search = search[l:]
// Finish routing if no remaining search and we are on an leaf node
if search == "" && (nn == nil || cn.parent == nil || cn.ppath != "") {
break
}
// Handle special case of trailing slash route with existing any route (see #1526)
if search == "" && path[len(path)-1] == '/' && cn.anyChildren != nil {
goto Any
if l != pl {
// No matching prefix, let's backtrack to the first possible alternative node of the decision path
nk, ok := backtrackToNextNodeKind(skind)
if !ok {
return // No other possibilities on the decision path
} else if nk == pkind {
goto Param
// NOTE: this case (backtracking from static node to previous any node) can not happen by current any matching logic. Any node is end of search currently
//} else if nk == akind {
// goto Any
} else {
// Not found (this should never be possible for static node we are looking currently)
return
}
}
// Attempt to go back up the tree on no matching prefix or no remaining search
if l != pl || search == "" {
if nn == nil { // Issue #1348
return // Not found
}
cn = nn
search = ns
if nk == pkind {
goto Param
} else if nk == akind {
goto Any
}
// The full prefix has matched, remove the prefix from the remaining search
search = search[l:]
searchIndex = searchIndex + l
// Finish routing if no remaining search and we are on an leaf node
if search == "" && cn.ppath != "" {
break
}
// Static node
if child = cn.findStaticChild(search[0]); child != nil {
// Save next
if cn.prefix[len(cn.prefix)-1] == '/' { // Issue #623
nk = pkind
nn = cn
ns = search
if search != "" {
if child := cn.findStaticChild(search[0]); child != nil {
cn = child
continue
}
cn = child
continue
}
Param:
// Param node
if child = cn.paramChildren; child != nil {
// Issue #378
if len(pvalues) == n {
continue
}
// Save next
if cn.prefix[len(cn.prefix)-1] == '/' { // Issue #623
nk = akind
nn = cn
ns = search
}
if child := cn.paramChildren; search != "" && child != nil {
cn = child
i, l := 0, len(search)
for ; i < l && search[i] != '/'; i++ {
@ -426,87 +434,39 @@ func (r *Router) Find(method, path string, c Context) {
pvalues[n] = search[:i]
n++
search = search[i:]
searchIndex = searchIndex + i
continue
}
Any:
// Any node
if cn = cn.anyChildren; cn != nil {
if child := cn.anyChildren; child != nil {
// If any node is found, use remaining path for pvalues
cn = child
pvalues[len(cn.pnames)-1] = search
break
}
// No node found, continue at stored next node
// or find nearest "any" route
if nn != nil {
// No next node to go down in routing (issue #954)
// Find nearest "any" route going up the routing tree
search = ns
np := nn.parent
// Consider param route one level up only
if cn = nn.paramChildren; cn != nil {
pos := strings.IndexByte(ns, '/')
if pos == -1 {
// If no slash is remaining in search string set param value
if len(cn.pnames) > 0 {
pvalues[len(cn.pnames)-1] = search
}
break
} else if pos > 0 {
// Otherwise continue route processing with restored next node
cn = nn
nn = nil
ns = ""
goto Param
}
}
// No param route found, try to resolve nearest any route
for {
np = nn.parent
if cn = nn.anyChildren; cn != nil {
break
}
if np == nil {
break // no further parent nodes in tree, abort
}
var str strings.Builder
str.WriteString(nn.prefix)
str.WriteString(search)
search = str.String()
nn = np
}
if cn != nil { // use the found "any" route and update path
pvalues[len(cn.pnames)-1] = search
break
}
// Let's backtrack to the first possible alternative node of the decision path
nk, ok := backtrackToNextNodeKind(akind)
if !ok {
return // No other possibilities on the decision path
} else if nk == pkind {
goto Param
} else if nk == akind {
goto Any
} else {
// Not found
return
}
return // Not found
}
ctx.handler = cn.findHandler(method)
ctx.path = cn.ppath
ctx.pnames = cn.pnames
// NOTE: Slow zone...
if ctx.handler == nil {
ctx.handler = cn.checkMethodNotAllowed()
// Dig further for any, might have an empty value for *, e.g.
// serving a directory. Issue #207.
if cn = cn.anyChildren; cn == nil {
return
}
if h := cn.findHandler(method); h != nil {
ctx.handler = h
} else {
ctx.handler = cn.checkMethodNotAllowed()
}
ctx.path = cn.ppath
ctx.pnames = cn.pnames
pvalues[len(cn.pnames)-1] = ""
}
return
}

View File

@ -730,26 +730,58 @@ func TestRouterMatchAny(t *testing.T) {
r := e.router
// Routes
r.Add(http.MethodGet, "/", func(Context) error {
return nil
})
r.Add(http.MethodGet, "/*", func(Context) error {
return nil
})
r.Add(http.MethodGet, "/users/*", func(Context) error {
return nil
})
r.Add(http.MethodGet, "/", handlerHelper("case", 1))
r.Add(http.MethodGet, "/*", handlerHelper("case", 2))
r.Add(http.MethodGet, "/users/*", handlerHelper("case", 3))
c := e.NewContext(nil, nil).(*context)
r.Find(http.MethodGet, "/", c)
assert.Equal(t, "", c.Param("*"))
c.handler(c)
assert.Equal(t, 1, c.Get("case"))
assert.Equal(t, "/", c.Get("path"))
r.Find(http.MethodGet, "/download", c)
c.handler(c)
assert.Equal(t, 2, c.Get("case"))
assert.Equal(t, "/*", c.Get("path"))
assert.Equal(t, "download", c.Param("*"))
r.Find(http.MethodGet, "/users/joe", c)
c.handler(c)
assert.Equal(t, 3, c.Get("case"))
assert.Equal(t, "/users/*", c.Get("path"))
assert.Equal(t, "joe", c.Param("*"))
}
// NOTE: this is to document current implementation. Last added route with `*` asterisk is always the match and no
// backtracking or more precise matching is done to find more suitable match.
//
// Current behaviour might not be correct or expected.
// But this is where we are without well defined requirements/rules how (multiple) asterisks work in route
func TestRouterAnyMatchesLastAddedAnyRoute(t *testing.T) {
e := New()
r := e.router
r.Add(http.MethodGet, "/users/*", handlerHelper("case", 1))
r.Add(http.MethodGet, "/users/*/action*", handlerHelper("case", 2))
c := e.NewContext(nil, nil).(*context)
r.Find(http.MethodGet, "/users/xxx/action/sea", c)
c.handler(c)
assert.Equal(t, "/users/*/action*", c.Get("path"))
assert.Equal(t, "xxx/action/sea", c.Param("*"))
// if we add another route then it is the last added and so it is matched
r.Add(http.MethodGet, "/users/*/action/search", handlerHelper("case", 3))
r.Find(http.MethodGet, "/users/xxx/action/sea", c)
c.handler(c)
assert.Equal(t, "/users/*/action/search", c.Get("path"))
assert.Equal(t, "xxx/action/sea", c.Param("*"))
}
// Issue #1739
func TestRouterMatchAnyPrefixIssue(t *testing.T) {
e := New()
@ -791,6 +823,130 @@ func TestRouterMatchAnyPrefixIssue(t *testing.T) {
assert.Equal(t, "users_prefix/", c.Param("*"))
}
func TestRouteMultiLevelBacktracking(t *testing.T) {
e := New()
r := e.router
r.Add(http.MethodGet, "/a/:b/c", handlerHelper("case", 1))
r.Add(http.MethodGet, "/a/c/d", handlerHelper("case", 2))
r.Add(http.MethodGet, "/:e/c/f", handlerHelper("case", 3))
c := e.NewContext(nil, nil).(*context)
r.Find(http.MethodGet, "/a/c/f", c)
c.handler(c)
assert.Equal(t, 3, c.Get("case"))
assert.Equal(t, "/:e/c/f", c.Get("path"))
}
// Issue #
func TestRouterBacktrackingFromParam(t *testing.T) {
e := New()
r := e.router
r.Add(http.MethodGet, "/*", handlerHelper("case", 1))
r.Add(http.MethodGet, "/users/:name/", handlerHelper("case", 2))
c := e.NewContext(nil, nil).(*context)
r.Find(http.MethodGet, "/users/firstname/no-match", c)
c.handler(c)
assert.Equal(t, 1, c.Get("case"))
assert.Equal(t, "/*", c.Get("path"))
assert.Equal(t, "users/firstname/no-match", c.Param("*"))
r.Find(http.MethodGet, "/users/firstname/", c)
c.handler(c)
assert.Equal(t, 2, c.Get("case"))
assert.Equal(t, "/users/:name/", c.Get("path"))
assert.Equal(t, "firstname", c.Param("name"))
}
func TestRouterBacktrackingFromParamAny(t *testing.T) {
e := New()
r := e.router
r.Add(http.MethodGet, "/*", handlerHelper("case", 1))
r.Add(http.MethodGet, "/:name/lastname", handlerHelper("case", 2))
c := e.NewContext(nil, nil).(*context)
r.Find(http.MethodGet, "/firstname/test", c)
c.handler(c)
assert.Equal(t, 1, c.Get("case"))
assert.Equal(t, "/*", c.Get("path"))
assert.Equal(t, "firstname/test", c.Param("*"))
r.Find(http.MethodGet, "/firstname", c)
c.handler(c)
assert.Equal(t, 1, c.Get("case"))
assert.Equal(t, "/*", c.Get("path"))
assert.Equal(t, "firstname", c.Param("*"))
r.Find(http.MethodGet, "/firstname/lastname", c)
c.handler(c)
assert.Equal(t, 2, c.Get("case"))
assert.Equal(t, "/:name/lastname", c.Get("path"))
assert.Equal(t, "firstname", c.Param("name"))
}
func TestRouterBacktrackingFromParamAny2(t *testing.T) {
e := New()
r := e.router
r.Add(http.MethodGet, "/*", handlerHelper("case", 1))
r.Add(http.MethodGet, "/:name", handlerHelper("case", 2))
r.Add(http.MethodGet, "/:name/lastname", handlerHelper("case", 3))
c := e.NewContext(nil, nil).(*context)
r.Find(http.MethodGet, "/firstname/test", c)
c.handler(c)
assert.Equal(t, 1, c.Get("case"))
assert.Equal(t, "/*", c.Get("path"))
assert.Equal(t, "firstname/test", c.Param("*"))
r.Find(http.MethodGet, "/firstname", c)
c.handler(c)
assert.Equal(t, 2, c.Get("case"))
assert.Equal(t, "/:name", c.Get("path"))
assert.Equal(t, "firstname", c.Param("name"))
r.Find(http.MethodGet, "/firstname/lastname", c)
c.handler(c)
assert.Equal(t, 3, c.Get("case"))
assert.Equal(t, "/:name/lastname", c.Get("path"))
assert.Equal(t, "firstname", c.Param("name"))
}
func TestRouterAnyCommonPath(t *testing.T) {
e := New()
r := e.router
r.Add(http.MethodGet, "/ab*", handlerHelper("case", 1))
r.Add(http.MethodGet, "/abcd", handlerHelper("case", 2))
r.Add(http.MethodGet, "/abcd*", handlerHelper("case", 3))
c := e.NewContext(nil, nil).(*context)
r.Find(http.MethodGet, "/abee", c)
c.handler(c)
assert.Equal(t, 1, c.Get("case"))
assert.Equal(t, "/ab*", c.Get("path"))
assert.Equal(t, "ee", c.Param("*"))
r.Find(http.MethodGet, "/abcd", c)
c.handler(c)
assert.Equal(t, "/abcd", c.Get("path"))
assert.Equal(t, 2, c.Get("case"))
r.Find(http.MethodGet, "/abcde", c)
c.handler(c)
assert.Equal(t, 3, c.Get("case"))
assert.Equal(t, "/abcd*", c.Get("path"))
assert.Equal(t, "e", c.Param("*"))
}
// TestRouterMatchAnySlash shall verify finding the best route
// for any routes with trailing slash requests
func TestRouterMatchAnySlash(t *testing.T) {