From 4fdc8a555d4a4714b5becc90b6c22492038810d7 Mon Sep 17 00:00:00 2001 From: Daniel Imfeld Date: Thu, 31 Mar 2016 16:14:54 -0400 Subject: [PATCH] Allow : and * in path segments. If the : or * occurs at the beginning of the segment, it must be escaped with a backslash. Fixes #29 --- README.md | 15 +++++++++++-- group.go | 2 +- tree.go | 36 ++++++++++++++++++++++++------- tree_test.go | 60 ++++++++++++++++++++++++---------------------------- 4 files changed, 70 insertions(+), 43 deletions(-) diff --git a/README.md b/README.md index a703553..05f058f 100644 --- a/README.md +++ b/README.md @@ -27,9 +27,20 @@ Some examples of valid URL patterns are: Note that all of the above URL patterns may exist concurrently in the router. -Path elements starting with : indicate a wildcard in the path. A wildcard will only match on a single path segment. That is, the pattern `/post/:postid` will match on `/post/1` or `/post/1/`, but not `/post/1/2`. +Path elements starting with `:` indicate a wildcard in the path. A wildcard will only match on a single path segment. That is, the pattern `/post/:postid` will match on `/post/1` or `/post/1/`, but not `/post/1/2`. -A path element starting with * is a catch-all, whose value will be a string containing all text in the URL matched by the wildcards. For example, with a pattern of `/images/*path` and a requested URL `images/abc/def`, path would contain `abc/def`. +A path element starting with `*` is a catch-all, whose value will be a string containing all text in the URL matched by the wildcards. For example, with a pattern of `/images/*path` and a requested URL `images/abc/def`, path would contain `abc/def`. + +#### Using : and * in routing patterns + +The characters `:` and `*` can be used at the beginning of a path segment by escaping them with a backslash. A double backslash at the beginning of a segment is interpreted as a single backslash. These escapes are only checked at the very beginning of a path segment; they are not necessary or processed elsewhere in a token. + +```go +router.GET("/foo/\\*starToken", handler) // matches /foo/*starToken +router.GET("/foo/star*inTheMiddle", handler) // matches /foo/star*inTheMiddle +router.GET("/foo/starBackslash\\*", handler) // matches /foo/starBackslash\* +router.GET("/foo/\\\\*backslashWithStar") // matches /foo/\*backslashWithStar +``` ### Routing Groups Lets you create a new group of routes with a given path prefix. Makes it easier to create clusters of paths like: diff --git a/group.go b/group.go index aea7715..ad242ba 100644 --- a/group.go +++ b/group.go @@ -98,7 +98,7 @@ func (g *Group) Handle(method string, path string, handler HandlerFunc) { path = path[:len(path)-1] } - node := g.mux.root.addPath(path[1:], nil) + node := g.mux.root.addPath(path[1:], nil, false) if addSlash { node.addSlash = true } diff --git a/tree.go b/tree.go index 5b06158..1e5a4c5 100644 --- a/tree.go +++ b/tree.go @@ -57,7 +57,7 @@ func (n *node) setHandler(verb string, handler HandlerFunc, implicitHead bool) { } } -func (n *node) addPath(path string, wildcards []string) *node { +func (n *node) addPath(path string, wildcards []string, inStaticToken bool) *node { leaf := len(path) == 0 if leaf { if wildcards != nil { @@ -90,6 +90,7 @@ func (n *node) addPath(path string, wildcards []string) *node { var tokenEnd int if c == '/' { + // Done processing the previous token, so reset inStaticToken to false. thisToken = "/" tokenEnd = 1 } else if nextSlash == -1 { @@ -101,7 +102,7 @@ func (n *node) addPath(path string, wildcards []string) *node { } remainingPath := path[tokenEnd:] - if c == '*' { + if c == '*' && !inStaticToken { // Token starts with a *, so it's a catch-all thisToken = thisToken[1:] if n.catchAllChild == nil { @@ -125,7 +126,7 @@ func (n *node) addPath(path string, wildcards []string) *node { n.catchAllChild.leafWildcardNames = wildcards return n.catchAllChild - } else if c == ':' { + } else if c == ':' && !inStaticToken { // Token starts with a : thisToken = thisToken[1:] @@ -139,22 +140,41 @@ func (n *node) addPath(path string, wildcards []string) *node { n.wildcardChild = &node{path: "wildcard"} } - return n.wildcardChild.addPath(remainingPath, wildcards) + return n.wildcardChild.addPath(remainingPath, wildcards, false) } else { - if strings.ContainsAny(thisToken, ":*") { - panic("* or : in middle of path component " + path) + // if strings.ContainsAny(thisToken, ":*") { + // panic("* or : in middle of path component " + path) + // } + + unescaped := false + if len(thisToken) >= 2 && !inStaticToken { + if thisToken[0] == '\\' && (thisToken[1] == '*' || thisToken[1] == ':' || thisToken[1] == '\\') { + // The token starts with a character escaped by a backslash. Drop the backslash. + c = thisToken[1] + thisToken = thisToken[1:] + unescaped = true + } } + // Set inStaticToken to ensure that the rest of this token is not mistaken + // for a wildcard if a prefix split occurs at a '*' or ':'. + inStaticToken = (c != '/') + // Do we have an existing node that starts with the same letter? for i, index := range n.staticIndices { if c == index { // Yes. Split it based on the common prefix of the existing // node and the new one. child, prefixSplit := n.splitCommonPrefix(i, thisToken) + child.priority++ n.sortStaticChild(i) - return child.addPath(path[prefixSplit:], wildcards) + if unescaped { + // Account for the removed backslash. + prefixSplit++ + } + return child.addPath(path[prefixSplit:], wildcards, inStaticToken) } } @@ -168,7 +188,7 @@ func (n *node) addPath(path string, wildcards []string) *node { n.staticIndices = append(n.staticIndices, c) n.staticChild = append(n.staticChild, child) } - return child.addPath(remainingPath, wildcards) + return child.addPath(remainingPath, wildcards, inStaticToken) } } diff --git a/tree_test.go b/tree_test.go index 7935ebb..094d3cb 100644 --- a/tree_test.go +++ b/tree_test.go @@ -2,7 +2,6 @@ package httptreemux import ( "net/http" - "strings" "testing" ) @@ -12,7 +11,7 @@ func dummyHandler(w http.ResponseWriter, r *http.Request, urlParams map[string]s func addPath(t *testing.T, tree *node, path string) { t.Logf("Adding path %s", path) - n := tree.addPath(path[1:], nil) + n := tree.addPath(path[1:], nil, false) handler := func(w http.ResponseWriter, r *http.Request, urlParams map[string]string) { urlParams["path"] = path } @@ -27,8 +26,6 @@ func testPath(t *testing.T, tree *node, path string, expectPath string, expected t.FailNow() } - expectCatchAll := strings.Contains(expectPath, "/*") - t.Log("Testing", path) n, foundHandler, paramList := tree.search("GET", path[1:]) if expectPath != "" && n == nil { @@ -44,10 +41,6 @@ func testPath(t *testing.T, tree *node, path string, expectPath string, expected return } - if expectCatchAll != n.isCatchAll { - t.Errorf("For path %s expectCatchAll %v but saw %v", path, expectCatchAll, n.isCatchAll) - } - handler, ok := n.leafHandler["GET"] if !ok { t.Errorf("Path %s returned node without handler", path) @@ -123,6 +116,8 @@ func TestTree(t *testing.T) { addPath(t, tree, "/images") addPath(t, tree, "/images/abc.jpg") addPath(t, tree, "/images/:imgname") + addPath(t, tree, "/images/\\*path") + addPath(t, tree, "/images/\\*patch") addPath(t, tree, "/images/*path") addPath(t, tree, "/ima") addPath(t, tree, "/ima/:par") @@ -133,6 +128,7 @@ func TestTree(t *testing.T) { addPath(t, tree, "/apples1") addPath(t, tree, "/appeasement") addPath(t, tree, "/appealing") + addPath(t, tree, "/date/\\:year/\\:month") addPath(t, tree, "/date/:year/:month") addPath(t, tree, "/date/:year/month") addPath(t, tree, "/date/:year/:month/abc") @@ -146,6 +142,10 @@ func TestTree(t *testing.T) { addPath(t, tree, "/users/:id/updatePassword") addPath(t, tree, "/:something/abc") addPath(t, tree, "/:something/def") + addPath(t, tree, "/apples/ab:cde/:fg/*hi") + addPath(t, tree, "/apples/ab*cde/:fg/*hi") + addPath(t, tree, "/apples/ab\\*cde/:fg/*hi") + addPath(t, tree, "/apples/ab*dde") testPath(t, tree, "/users/abc/updatePassword", "/users/:id/updatePassword", map[string]string{"id": "abc"}) @@ -197,6 +197,22 @@ func TestTree(t *testing.T) { testPath(t, tree, "/post/ab%2fdef/page/2%2f", "/post/:post/page/:page", map[string]string{"post": "ab/def", "page": "2/"}) + // Test paths with escaped wildcard characters. + testPath(t, tree, "/images/*path", "/images/\\*path", nil) + testPath(t, tree, "/images/*patch", "/images/\\*patch", nil) + testPath(t, tree, "/date/:year/:month", "/date/\\:year/\\:month", nil) + testPath(t, tree, "/apples/ab*cde/lala/baba/dada", "/apples/ab*cde/:fg/*hi", + map[string]string{"fg": "lala", "hi": "baba/dada"}) + testPath(t, tree, "/apples/ab\\*cde/lala/baba/dada", "/apples/ab\\*cde/:fg/*hi", + map[string]string{"fg": "lala", "hi": "baba/dada"}) + testPath(t, tree, "/apples/ab:cde/:fg/*hi", "/apples/ab:cde/:fg/*hi", + map[string]string{"fg": ":fg", "hi": "*hi"}) + testPath(t, tree, "/apples/ab*cde/:fg/*hi", "/apples/ab*cde/:fg/*hi", + map[string]string{"fg": ":fg", "hi": "*hi"}) + testPath(t, tree, "/apples/ab*cde/one/two/three", "/apples/ab*cde/:fg/*hi", + map[string]string{"fg": "one", "hi": "two/three"}) + testPath(t, tree, "/apples/ab*dde", "/apples/ab*dde", nil) + testPath(t, tree, "/ima/bcd/fgh", "", nil) testPath(t, tree, "/date/2014//month", "", nil) testPath(t, tree, "/date/2014/05/", "", nil) // Empty catchall should not match @@ -209,7 +225,7 @@ func TestTree(t *testing.T) { t.Log("Test retrieval of duplicate paths") params := make(map[string]string) p := "date/:year/:month/abc" - n := tree.addPath(p, nil) + n := tree.addPath(p, nil, false) if n == nil { t.Errorf("Duplicate add of %s didn't return a node", p) } else { @@ -247,7 +263,7 @@ func TestPanics(t *testing.T) { defer panicHandler() tree := &node{path: "/"} for _, path := range p { - tree.addPath(path, nil) + tree.addPath(path, nil, false) } } @@ -277,26 +293,6 @@ func TestPanics(t *testing.T) { t.Error("Expected panic when adding a duplicate handler for a pattern") } - addPathPanic("abc/ab:cd") - if !sawPanic { - t.Error("Expected panic with : in middle of path segment") - } - - addPathPanic("abc/ab", "abc/ab:cd") - if !sawPanic { - t.Error("Expected panic with : in middle of path segment with existing path") - } - - addPathPanic("abc/ab*cd") - if !sawPanic { - t.Error("Expected panic with * in middle of path segment") - } - - addPathPanic("abc/ab", "abc/ab*cd") - if !sawPanic { - t.Error("Expected panic with * in middle of path segment with existing path") - } - twoPathPanic := func(first, second string) { addPathPanic(first, second) if !sawPanic { @@ -333,7 +329,7 @@ func BenchmarkTreeOneStatic(b *testing.B) { "GET": dummyHandler, }, } - tree.addPath("abc", nil) + tree.addPath("abc", nil, false) b.ResetTimer() for i := 0; i < b.N; i++ { @@ -349,7 +345,7 @@ func BenchmarkTreeOneParam(b *testing.B) { }, } b.ReportAllocs() - tree.addPath(":abc", nil) + tree.addPath(":abc", nil, false) b.ResetTimer() for i := 0; i < b.N; i++ {