diff --git a/tree.go b/tree.go index 95f31d4f..aed2e50c 100644 --- a/tree.go +++ b/tree.go @@ -446,7 +446,29 @@ func (n *node) findRoute(rctx *Context, method methodTyp, path string) *node { } if ntyp == ntRegexp && xn.rex != nil { - if !xn.rex.MatchString(xsearch[:p]) { + // The tail delimiter can legitimately appear inside the value a + // regexp param is meant to capture (e.g. a "-" delimiter with a + // UUID value). Splitting on the first delimiter would truncate the + // value and fail to match, so advance to subsequent delimiters + // within the same path segment until the regexp matches. + for !xn.rex.MatchString(xsearch[:p]) { + next := -1 + if p < len(xsearch) { + next = strings.IndexByte(xsearch[p+1:], xn.tail) + } + if next < 0 { + p = -1 + break + } + np := p + 1 + next + // don't let a regexp param match across path segments + if strings.IndexByte(xsearch[:np], '/') != -1 { + p = -1 + break + } + p = np + } + if p < 0 { continue } } else if strings.IndexByte(xsearch[:p], '/') != -1 { diff --git a/tree_test.go b/tree_test.go index c07e8309..a1d515f9 100644 --- a/tree_test.go +++ b/tree_test.go @@ -414,6 +414,50 @@ func TestTreeRegexMatchWholeParam(t *testing.T) { } } +// A regexp param can legitimately capture a value that contains the byte used +// as the tail delimiter for the next part of the same path segment (e.g. a UUID +// "8-4-4-4-12" value followed by a literal "-" then another param). The router +// must not truncate the value at the first delimiter. See issue #970. +func TestTreeRegexpTailInValue(t *testing.T) { + hStub1 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) + + rctx := NewRouteContext() + tr := &node{} + // {uuid} contains '-', which is also the delimiter before {date}. + tr.InsertRoute(mGET, "/test/{uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}-{date}", hStub1) + + tests := []struct { + url string + expectedHandler http.Handler + uuid string + date string + }{ + {url: "/test/f9772163-44e7-49b1-9c8c-36b8d023aa6b-2024-01-01", expectedHandler: hStub1, uuid: "f9772163-44e7-49b1-9c8c-36b8d023aa6b", date: "2024-01-01"}, + // value that does not satisfy the regexp must not match + {url: "/test/notauuid-2024-01-01", expectedHandler: nil}, + // missing trailing {date} segment must not match + {url: "/test/f9772163-44e7-49b1-9c8c-36b8d023aa6b", expectedHandler: nil}, + } + + for _, tc := range tests { + rctx.Reset() + _, _, handler := tr.FindRoute(rctx, mGET, tc.url) + if fmt.Sprintf("%v", tc.expectedHandler) != fmt.Sprintf("%v", handler) { + t.Errorf("url %v: expecting handler:%v , got:%v", tc.url, tc.expectedHandler, handler) + continue + } + if tc.expectedHandler == nil { + continue + } + if got := rctx.URLParam("uuid"); got != tc.uuid { + t.Errorf("url %v: uuid = %q, want %q", tc.url, got, tc.uuid) + } + if got := rctx.URLParam("date"); got != tc.date { + t.Errorf("url %v: date = %q, want %q", tc.url, got, tc.date) + } + } +} + func TestTreeFindPattern(t *testing.T) { hStub1 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {}) hStub2 := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})