New upstream snapshot.
Debian Janitor
2 years ago
8 | 8 | - 1.12.x |
9 | 9 | - 1.13.x |
10 | 10 | - master |
11 | matrix: | |
12 | allow_failures: | |
13 | - go: master | |
14 | fast_finish: true | |
11 | 15 | before_install: |
12 | 16 | - go get github.com/mattn/goveralls |
13 | 17 | script: |
155 | 155 | if r.Header.Get("Access-Control-Request-Method") != "" { |
156 | 156 | // Set CORS headers |
157 | 157 | header := w.Header() |
158 | header.Set("Access-Control-Allow-Methods", r.Header.Get("Allow")) | |
158 | header.Set("Access-Control-Allow-Methods", header.Get("Allow")) | |
159 | 159 | header.Set("Access-Control-Allow-Origin", "*") |
160 | 160 | } |
161 | 161 |
0 | golang-github-julienschmidt-httprouter (1.3.0-2) UNRELEASED; urgency=medium | |
0 | golang-github-julienschmidt-httprouter (1.3.0+git20200921.1.fe77dd0-1) UNRELEASED; urgency=medium | |
1 | 1 | |
2 | 2 | * Remove constraints unnecessary since stretch: |
3 | 3 | + Build-Depends: Drop versioned constraint on dh-golang. |
4 | * New upstream snapshot. | |
4 | 5 | |
5 | -- Debian Janitor <janitor@jelmer.uk> Tue, 15 Jun 2021 18:03:45 -0000 | |
6 | -- Debian Janitor <janitor@jelmer.uk> Sat, 02 Apr 2022 05:33:19 -0000 | |
6 | 7 | |
7 | 8 | golang-github-julienschmidt-httprouter (1.3.0-1) unstable; urgency=medium |
8 | 9 |
18 | 18 | // |
19 | 19 | // If the result of this process is an empty string, "/" is returned |
20 | 20 | func CleanPath(p string) string { |
21 | const stackBufSize = 128 | |
22 | ||
21 | 23 | // Turn empty string into "/" |
22 | 24 | if p == "" { |
23 | 25 | return "/" |
24 | 26 | } |
25 | 27 | |
28 | // Reasonably sized buffer on stack to avoid allocations in the common case. | |
29 | // If a larger buffer is required, it gets allocated dynamically. | |
30 | buf := make([]byte, 0, stackBufSize) | |
31 | ||
26 | 32 | n := len(p) |
27 | var buf []byte | |
28 | 33 | |
29 | 34 | // Invariants: |
30 | 35 | // reading from path; r is index of next byte to process. |
36 | 41 | |
37 | 42 | if p[0] != '/' { |
38 | 43 | r = 0 |
39 | buf = make([]byte, n+1) | |
44 | ||
45 | if n+1 > stackBufSize { | |
46 | buf = make([]byte, n+1) | |
47 | } else { | |
48 | buf = buf[:n+1] | |
49 | } | |
40 | 50 | buf[0] = '/' |
41 | 51 | } |
42 | 52 | |
43 | 53 | trailing := n > 1 && p[n-1] == '/' |
44 | 54 | |
45 | 55 | // A bit more clunky without a 'lazybuf' like the path package, but the loop |
46 | // gets completely inlined (bufApp). So in contrast to the path package this | |
47 | // loop has no expensive function calls (except 1x make) | |
56 | // gets completely inlined (bufApp calls). | |
57 | // So in contrast to the path package this loop has no expensive function | |
58 | // calls (except make, if needed). | |
48 | 59 | |
49 | 60 | for r < n { |
50 | 61 | switch { |
68 | 79 | // can backtrack |
69 | 80 | w-- |
70 | 81 | |
71 | if buf == nil { | |
82 | if len(buf) == 0 { | |
72 | 83 | for w > 1 && p[w] != '/' { |
73 | 84 | w-- |
74 | 85 | } |
80 | 91 | } |
81 | 92 | |
82 | 93 | default: |
83 | // real path element. | |
84 | // add slash if needed | |
94 | // Real path element. | |
95 | // Add slash if needed | |
85 | 96 | if w > 1 { |
86 | 97 | bufApp(&buf, p, w, '/') |
87 | 98 | w++ |
88 | 99 | } |
89 | 100 | |
90 | // copy element | |
101 | // Copy element | |
91 | 102 | for r < n && p[r] != '/' { |
92 | 103 | bufApp(&buf, p, w, p[r]) |
93 | 104 | w++ |
96 | 107 | } |
97 | 108 | } |
98 | 109 | |
99 | // re-append trailing slash | |
110 | // Re-append trailing slash | |
100 | 111 | if trailing && w > 1 { |
101 | 112 | bufApp(&buf, p, w, '/') |
102 | 113 | w++ |
103 | 114 | } |
104 | 115 | |
105 | if buf == nil { | |
116 | // If the original string was not modified (or only shortened at the end), | |
117 | // return the respective substring of the original string. | |
118 | // Otherwise return a new string from the buffer. | |
119 | if len(buf) == 0 { | |
106 | 120 | return p[:w] |
107 | 121 | } |
108 | 122 | return string(buf[:w]) |
109 | 123 | } |
110 | 124 | |
111 | // internal helper to lazily create a buffer if necessary | |
125 | // Internal helper to lazily create a buffer if necessary. | |
126 | // Calls to this function get inlined. | |
112 | 127 | func bufApp(buf *[]byte, s string, w int, c byte) { |
113 | if *buf == nil { | |
128 | b := *buf | |
129 | if len(b) == 0 { | |
130 | // No modification of the original string so far. | |
131 | // If the next character is the same as in the original string, we do | |
132 | // not yet have to allocate a buffer. | |
114 | 133 | if s[w] == c { |
115 | 134 | return |
116 | 135 | } |
117 | 136 | |
118 | *buf = make([]byte, len(s)) | |
119 | copy(*buf, s[:w]) | |
137 | // Otherwise use either the stack buffer, if it is large enough, or | |
138 | // allocate a new buffer on the heap, and copy all previous characters. | |
139 | if l := len(s); l > cap(b) { | |
140 | *buf = make([]byte, len(s)) | |
141 | } else { | |
142 | *buf = (*buf)[:l] | |
143 | } | |
144 | b = *buf | |
145 | ||
146 | copy(b, s[:w]) | |
120 | 147 | } |
121 | (*buf)[w] = c | |
148 | b[w] = c | |
122 | 149 | } |
5 | 5 | package httprouter |
6 | 6 | |
7 | 7 | import ( |
8 | "runtime" | |
8 | "strings" | |
9 | 9 | "testing" |
10 | 10 | ) |
11 | 11 | |
12 | var cleanTests = []struct { | |
12 | type cleanPathTest struct { | |
13 | 13 | path, result string |
14 | }{ | |
14 | } | |
15 | ||
16 | var cleanTests = []cleanPathTest{ | |
15 | 17 | // Already clean |
16 | 18 | {"/", "/"}, |
17 | 19 | {"/abc", "/abc"}, |
78 | 80 | if testing.Short() { |
79 | 81 | t.Skip("skipping malloc count in short mode") |
80 | 82 | } |
81 | if runtime.GOMAXPROCS(0) > 1 { | |
82 | t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1") | |
83 | return | |
84 | } | |
85 | 83 | |
86 | 84 | for _, test := range cleanTests { |
85 | test := test | |
87 | 86 | allocs := testing.AllocsPerRun(100, func() { CleanPath(test.result) }) |
88 | 87 | if allocs > 0 { |
89 | 88 | t.Errorf("CleanPath(%q): %v allocs, want zero", test.result, allocs) |
90 | 89 | } |
91 | 90 | } |
92 | 91 | } |
92 | ||
93 | func BenchmarkPathClean(b *testing.B) { | |
94 | b.ReportAllocs() | |
95 | ||
96 | for i := 0; i < b.N; i++ { | |
97 | for _, test := range cleanTests { | |
98 | CleanPath(test.path) | |
99 | } | |
100 | } | |
101 | } | |
102 | ||
103 | func genLongPaths() (testPaths []cleanPathTest) { | |
104 | for i := 1; i <= 1234; i++ { | |
105 | ss := strings.Repeat("a", i) | |
106 | ||
107 | correctPath := "/" + ss | |
108 | testPaths = append(testPaths, cleanPathTest{ | |
109 | path: correctPath, | |
110 | result: correctPath, | |
111 | }, cleanPathTest{ | |
112 | path: ss, | |
113 | result: correctPath, | |
114 | }, cleanPathTest{ | |
115 | path: "//" + ss, | |
116 | result: correctPath, | |
117 | }, cleanPathTest{ | |
118 | path: "/" + ss + "/b/..", | |
119 | result: correctPath, | |
120 | }) | |
121 | } | |
122 | return testPaths | |
123 | } | |
124 | ||
125 | func TestPathCleanLong(t *testing.T) { | |
126 | cleanTests := genLongPaths() | |
127 | ||
128 | for _, test := range cleanTests { | |
129 | if s := CleanPath(test.path); s != test.result { | |
130 | t.Errorf("CleanPath(%q) = %q, want %q", test.path, s, test.result) | |
131 | } | |
132 | if s := CleanPath(test.result); s != test.result { | |
133 | t.Errorf("CleanPath(%q) = %q, want %q", test.result, s, test.result) | |
134 | } | |
135 | } | |
136 | } | |
137 | ||
138 | func BenchmarkPathCleanLong(b *testing.B) { | |
139 | cleanTests := genLongPaths() | |
140 | b.ResetTimer() | |
141 | b.ReportAllocs() | |
142 | ||
143 | for i := 0; i < b.N; i++ { | |
144 | for _, test := range cleanTests { | |
145 | CleanPath(test.path) | |
146 | } | |
147 | } | |
148 | } |
33 | 33 | // The router matches incoming requests by the request method and the path. |
34 | 34 | // If a handle is registered for this path and method, the router delegates the |
35 | 35 | // request to that function. |
36 | // For the methods GET, POST, PUT, PATCH and DELETE shortcut functions exist to | |
36 | // For the methods GET, POST, PUT, PATCH, DELETE and OPTIONS shortcut functions exist to | |
37 | 37 | // register handles, for all other methods router.Handle can be used. |
38 | 38 | // |
39 | 39 | // The registered path, against which the router matches incoming requests, can |
79 | 79 | "context" |
80 | 80 | "net/http" |
81 | 81 | "strings" |
82 | "sync" | |
82 | 83 | ) |
83 | 84 | |
84 | 85 | // Handle is a function that can be registered to a route to handle HTTP |
85 | 86 | // requests. Like http.HandlerFunc, but has a third parameter for the values of |
86 | // wildcards (variables). | |
87 | // wildcards (path variables). | |
87 | 88 | type Handle func(http.ResponseWriter, *http.Request, Params) |
88 | 89 | |
89 | 90 | // Param is a single URL parameter, consisting of a key and a value. |
100 | 101 | // ByName returns the value of the first Param which key matches the given name. |
101 | 102 | // If no matching Param is found, an empty string is returned. |
102 | 103 | func (ps Params) ByName(name string) string { |
103 | for i := range ps { | |
104 | if ps[i].Key == name { | |
105 | return ps[i].Value | |
104 | for _, p := range ps { | |
105 | if p.Key == name { | |
106 | return p.Value | |
106 | 107 | } |
107 | 108 | } |
108 | 109 | return "" |
120 | 121 | return p |
121 | 122 | } |
122 | 123 | |
124 | // MatchedRoutePathParam is the Param name under which the path of the matched | |
125 | // route is stored, if Router.SaveMatchedRoutePath is set. | |
126 | var MatchedRoutePathParam = "$matchedRoutePath" | |
127 | ||
128 | // MatchedRoutePath retrieves the path of the matched route. | |
129 | // Router.SaveMatchedRoutePath must have been enabled when the respective | |
130 | // handler was added, otherwise this function always returns an empty string. | |
131 | func (ps Params) MatchedRoutePath() string { | |
132 | return ps.ByName(MatchedRoutePathParam) | |
133 | } | |
134 | ||
123 | 135 | // Router is a http.Handler which can be used to dispatch requests to different |
124 | 136 | // handler functions via configurable routes |
125 | 137 | type Router struct { |
126 | 138 | trees map[string]*node |
127 | 139 | |
140 | paramsPool sync.Pool | |
141 | maxParams uint16 | |
142 | ||
143 | // If enabled, adds the matched route path onto the http.Request context | |
144 | // before invoking the handler. | |
145 | // The matched route path is only added to handlers of routes that were | |
146 | // registered when this option was enabled. | |
147 | SaveMatchedRoutePath bool | |
148 | ||
128 | 149 | // Enables automatic redirection if the current route can't be matched but a |
129 | 150 | // handler for the path with (without) the trailing slash exists. |
130 | 151 | // For example if /foo/ is requested but a route only exists for /foo, the |
131 | 152 | // client is redirected to /foo with http status code 301 for GET requests |
132 | // and 307 for all other request methods. | |
153 | // and 308 for all other request methods. | |
133 | 154 | RedirectTrailingSlash bool |
134 | 155 | |
135 | 156 | // If enabled, the router tries to fix the current request path, if no |
137 | 158 | // First superfluous path elements like ../ or // are removed. |
138 | 159 | // Afterwards the router does a case-insensitive lookup of the cleaned path. |
139 | 160 | // If a handle can be found for this route, the router makes a redirection |
140 | // to the corrected path with status code 301 for GET requests and 307 for | |
161 | // to the corrected path with status code 301 for GET requests and 308 for | |
141 | 162 | // all other request methods. |
142 | 163 | // For example /FOO and /..//Foo could be redirected to /foo. |
143 | 164 | // RedirectTrailingSlash is independent of this option. |
197 | 218 | } |
198 | 219 | } |
199 | 220 | |
221 | func (r *Router) getParams() *Params { | |
222 | ps, _ := r.paramsPool.Get().(*Params) | |
223 | *ps = (*ps)[0:0] // reset slice | |
224 | return ps | |
225 | } | |
226 | ||
227 | func (r *Router) putParams(ps *Params) { | |
228 | if ps != nil { | |
229 | r.paramsPool.Put(ps) | |
230 | } | |
231 | } | |
232 | ||
233 | func (r *Router) saveMatchedRoutePath(path string, handle Handle) Handle { | |
234 | return func(w http.ResponseWriter, req *http.Request, ps Params) { | |
235 | if ps == nil { | |
236 | psp := r.getParams() | |
237 | ps = (*psp)[0:1] | |
238 | ps[0] = Param{Key: MatchedRoutePathParam, Value: path} | |
239 | handle(w, req, ps) | |
240 | r.putParams(psp) | |
241 | } else { | |
242 | ps = append(ps, Param{Key: MatchedRoutePathParam, Value: path}) | |
243 | handle(w, req, ps) | |
244 | } | |
245 | } | |
246 | } | |
247 | ||
200 | 248 | // GET is a shortcut for router.Handle(http.MethodGet, path, handle) |
201 | 249 | func (r *Router) GET(path string, handle Handle) { |
202 | 250 | r.Handle(http.MethodGet, path, handle) |
241 | 289 | // frequently used, non-standardized or custom methods (e.g. for internal |
242 | 290 | // communication with a proxy). |
243 | 291 | func (r *Router) Handle(method, path string, handle Handle) { |
292 | varsCount := uint16(0) | |
293 | ||
294 | if method == "" { | |
295 | panic("method must not be empty") | |
296 | } | |
244 | 297 | if len(path) < 1 || path[0] != '/' { |
245 | 298 | panic("path must begin with '/' in path '" + path + "'") |
299 | } | |
300 | if handle == nil { | |
301 | panic("handle must not be nil") | |
302 | } | |
303 | ||
304 | if r.SaveMatchedRoutePath { | |
305 | varsCount++ | |
306 | handle = r.saveMatchedRoutePath(path, handle) | |
246 | 307 | } |
247 | 308 | |
248 | 309 | if r.trees == nil { |
258 | 319 | } |
259 | 320 | |
260 | 321 | root.addRoute(path, handle) |
322 | ||
323 | // Update maxParams | |
324 | if paramsCount := countParams(path); paramsCount+varsCount > r.maxParams { | |
325 | r.maxParams = paramsCount + varsCount | |
326 | } | |
327 | ||
328 | // Lazy-init paramsPool alloc func | |
329 | if r.paramsPool.New == nil && r.maxParams > 0 { | |
330 | r.paramsPool.New = func() interface{} { | |
331 | ps := make(Params, 0, r.maxParams) | |
332 | return &ps | |
333 | } | |
334 | } | |
261 | 335 | } |
262 | 336 | |
263 | 337 | // Handler is an adapter which allows the usage of an http.Handler as a |
318 | 392 | // the same path with an extra / without the trailing slash should be performed. |
319 | 393 | func (r *Router) Lookup(method, path string) (Handle, Params, bool) { |
320 | 394 | if root := r.trees[method]; root != nil { |
321 | return root.getValue(path) | |
395 | handle, ps, tsr := root.getValue(path, r.getParams) | |
396 | if handle == nil { | |
397 | r.putParams(ps) | |
398 | return nil, nil, tsr | |
399 | } | |
400 | if ps == nil { | |
401 | return handle, nil, tsr | |
402 | } | |
403 | return handle, *ps, tsr | |
322 | 404 | } |
323 | 405 | return nil, nil, false |
324 | 406 | } |
346 | 428 | continue |
347 | 429 | } |
348 | 430 | |
349 | handle, _, _ := r.trees[method].getValue(path) | |
431 | handle, _, _ := r.trees[method].getValue(path, nil) | |
350 | 432 | if handle != nil { |
351 | 433 | // Add request method to list of allowed methods |
352 | 434 | allowed = append(allowed, method) |
370 | 452 | // return as comma separated list |
371 | 453 | return strings.Join(allowed, ", ") |
372 | 454 | } |
373 | return | |
455 | ||
456 | return allow | |
374 | 457 | } |
375 | 458 | |
376 | 459 | // ServeHTTP makes the router implement the http.Handler interface. |
382 | 465 | path := req.URL.Path |
383 | 466 | |
384 | 467 | if root := r.trees[req.Method]; root != nil { |
385 | if handle, ps, tsr := root.getValue(path); handle != nil { | |
386 | handle(w, req, ps) | |
468 | if handle, ps, tsr := root.getValue(path, r.getParams); handle != nil { | |
469 | if ps != nil { | |
470 | handle(w, req, *ps) | |
471 | r.putParams(ps) | |
472 | } else { | |
473 | handle(w, req, nil) | |
474 | } | |
387 | 475 | return |
388 | 476 | } else if req.Method != http.MethodConnect && path != "/" { |
389 | code := 301 // Permanent redirect, request with GET method | |
477 | // Moved Permanently, request with GET method | |
478 | code := http.StatusMovedPermanently | |
390 | 479 | if req.Method != http.MethodGet { |
391 | // Temporary redirect, request with same method | |
392 | // As of Go 1.3, Go does not support status code 308. | |
393 | code = 307 | |
480 | // Permanent Redirect, request with same method | |
481 | code = http.StatusPermanentRedirect | |
394 | 482 | } |
395 | 483 | |
396 | 484 | if tsr && r.RedirectTrailingSlash { |
410 | 498 | r.RedirectTrailingSlash, |
411 | 499 | ) |
412 | 500 | if found { |
413 | req.URL.Path = string(fixedPath) | |
501 | req.URL.Path = fixedPath | |
414 | 502 | http.Redirect(w, req, req.URL.String(), code) |
415 | 503 | return |
416 | 504 | } |
163 | 163 | } |
164 | 164 | } |
165 | 165 | |
166 | func TestRouterRoot(t *testing.T) { | |
167 | router := New() | |
166 | func TestRouterInvalidInput(t *testing.T) { | |
167 | router := New() | |
168 | ||
169 | handle := func(_ http.ResponseWriter, _ *http.Request, _ Params) {} | |
170 | ||
168 | 171 | recv := catchPanic(func() { |
169 | router.GET("noSlashRoot", nil) | |
172 | router.Handle("", "/", handle) | |
173 | }) | |
174 | if recv == nil { | |
175 | t.Fatal("registering empty method did not panic") | |
176 | } | |
177 | ||
178 | recv = catchPanic(func() { | |
179 | router.GET("", handle) | |
180 | }) | |
181 | if recv == nil { | |
182 | t.Fatal("registering empty path did not panic") | |
183 | } | |
184 | ||
185 | recv = catchPanic(func() { | |
186 | router.GET("noSlashRoot", handle) | |
170 | 187 | }) |
171 | 188 | if recv == nil { |
172 | 189 | t.Fatal("registering path not beginning with '/' did not panic") |
190 | } | |
191 | ||
192 | recv = catchPanic(func() { | |
193 | router.GET("/", nil) | |
194 | }) | |
195 | if recv == nil { | |
196 | t.Fatal("registering nil handler did not panic") | |
173 | 197 | } |
174 | 198 | } |
175 | 199 | |
394 | 418 | code int |
395 | 419 | location string |
396 | 420 | }{ |
397 | {"/path/", 301, "/path"}, // TSR -/ | |
398 | {"/dir", 301, "/dir/"}, // TSR +/ | |
399 | {"", 301, "/"}, // TSR +/ | |
400 | {"/PATH", 301, "/path"}, // Fixed Case | |
401 | {"/DIR/", 301, "/dir/"}, // Fixed Case | |
402 | {"/PATH/", 301, "/path"}, // Fixed Case -/ | |
403 | {"/DIR", 301, "/dir/"}, // Fixed Case +/ | |
404 | {"/../path", 301, "/path"}, // CleanPath | |
405 | {"/nope", 404, ""}, // NotFound | |
421 | {"/path/", http.StatusMovedPermanently, "/path"}, // TSR -/ | |
422 | {"/dir", http.StatusMovedPermanently, "/dir/"}, // TSR +/ | |
423 | {"", http.StatusMovedPermanently, "/"}, // TSR +/ | |
424 | {"/PATH", http.StatusMovedPermanently, "/path"}, // Fixed Case | |
425 | {"/DIR/", http.StatusMovedPermanently, "/dir/"}, // Fixed Case | |
426 | {"/PATH/", http.StatusMovedPermanently, "/path"}, // Fixed Case -/ | |
427 | {"/DIR", http.StatusMovedPermanently, "/dir/"}, // Fixed Case +/ | |
428 | {"/../path", http.StatusMovedPermanently, "/path"}, // CleanPath | |
429 | {"/nope", http.StatusNotFound, ""}, // NotFound | |
406 | 430 | } |
407 | 431 | for _, tr := range testRoutes { |
408 | 432 | r, _ := http.NewRequest(http.MethodGet, tr.route, nil) |
409 | 433 | w := httptest.NewRecorder() |
410 | 434 | router.ServeHTTP(w, r) |
411 | if !(w.Code == tr.code && (w.Code == 404 || fmt.Sprint(w.Header().Get("Location")) == tr.location)) { | |
435 | if !(w.Code == tr.code && (w.Code == http.StatusNotFound || fmt.Sprint(w.Header().Get("Location")) == tr.location)) { | |
412 | 436 | t.Errorf("NotFound handling route %s failed: Code=%d, Header=%v", tr.route, w.Code, w.Header().Get("Location")) |
413 | 437 | } |
414 | 438 | } |
416 | 440 | // Test custom not found handler |
417 | 441 | var notFound bool |
418 | 442 | router.NotFound = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) { |
419 | rw.WriteHeader(404) | |
443 | rw.WriteHeader(http.StatusNotFound) | |
420 | 444 | notFound = true |
421 | 445 | }) |
422 | 446 | r, _ := http.NewRequest(http.MethodGet, "/nope", nil) |
423 | 447 | w := httptest.NewRecorder() |
424 | 448 | router.ServeHTTP(w, r) |
425 | if !(w.Code == 404 && notFound == true) { | |
449 | if !(w.Code == http.StatusNotFound && notFound == true) { | |
426 | 450 | t.Errorf("Custom NotFound handler failed: Code=%d, Header=%v", w.Code, w.Header()) |
427 | 451 | } |
428 | 452 | |
429 | // Test other method than GET (want 307 instead of 301) | |
453 | // Test other method than GET (want 308 instead of 301) | |
430 | 454 | router.PATCH("/path", handlerFunc) |
431 | 455 | r, _ = http.NewRequest(http.MethodPatch, "/path/", nil) |
432 | 456 | w = httptest.NewRecorder() |
433 | 457 | router.ServeHTTP(w, r) |
434 | if !(w.Code == 307 && fmt.Sprint(w.Header()) == "map[Location:[/path]]") { | |
458 | if !(w.Code == http.StatusPermanentRedirect && fmt.Sprint(w.Header()) == "map[Location:[/path]]") { | |
435 | 459 | t.Errorf("Custom NotFound handler failed: Code=%d, Header=%v", w.Code, w.Header()) |
436 | 460 | } |
437 | 461 | |
441 | 465 | r, _ = http.NewRequest(http.MethodGet, "/", nil) |
442 | 466 | w = httptest.NewRecorder() |
443 | 467 | router.ServeHTTP(w, r) |
444 | if !(w.Code == 404) { | |
468 | if !(w.Code == http.StatusNotFound) { | |
445 | 469 | t.Errorf("NotFound handling route / failed: Code=%d", w.Code) |
446 | 470 | } |
447 | 471 | } |
494 | 518 | |
495 | 519 | // insert route and try again |
496 | 520 | router.GET("/user/:name", wantHandle) |
497 | ||
498 | 521 | handle, params, _ := router.Lookup(http.MethodGet, "/user/gopher") |
499 | 522 | if handle == nil { |
500 | 523 | t.Fatal("Got no handle!") |
504 | 527 | t.Fatal("Routing failed!") |
505 | 528 | } |
506 | 529 | } |
507 | ||
508 | 530 | if !reflect.DeepEqual(params, wantParams) { |
509 | 531 | t.Fatalf("Wrong parameter values: want %v, got %v", wantParams, params) |
532 | } | |
533 | routed = false | |
534 | ||
535 | // route without param | |
536 | router.GET("/user", wantHandle) | |
537 | handle, params, _ = router.Lookup(http.MethodGet, "/user") | |
538 | if handle == nil { | |
539 | t.Fatal("Got no handle!") | |
540 | } else { | |
541 | handle(nil, nil, nil) | |
542 | if !routed { | |
543 | t.Fatal("Routing failed!") | |
544 | } | |
545 | } | |
546 | if params != nil { | |
547 | t.Fatalf("Wrong parameter values: want %v, got %v", nil, params) | |
510 | 548 | } |
511 | 549 | |
512 | 550 | handle, _, tsr = router.Lookup(http.MethodGet, "/user/gopher/") |
571 | 609 | } |
572 | 610 | } |
573 | 611 | |
612 | func TestRouterMatchedRoutePath(t *testing.T) { | |
613 | route1 := "/user/:name" | |
614 | routed1 := false | |
615 | handle1 := func(_ http.ResponseWriter, req *http.Request, ps Params) { | |
616 | route := ps.MatchedRoutePath() | |
617 | if route != route1 { | |
618 | t.Fatalf("Wrong matched route: want %s, got %s", route1, route) | |
619 | } | |
620 | routed1 = true | |
621 | } | |
622 | ||
623 | route2 := "/user/:name/details" | |
624 | routed2 := false | |
625 | handle2 := func(_ http.ResponseWriter, req *http.Request, ps Params) { | |
626 | route := ps.MatchedRoutePath() | |
627 | if route != route2 { | |
628 | t.Fatalf("Wrong matched route: want %s, got %s", route2, route) | |
629 | } | |
630 | routed2 = true | |
631 | } | |
632 | ||
633 | route3 := "/" | |
634 | routed3 := false | |
635 | handle3 := func(_ http.ResponseWriter, req *http.Request, ps Params) { | |
636 | route := ps.MatchedRoutePath() | |
637 | if route != route3 { | |
638 | t.Fatalf("Wrong matched route: want %s, got %s", route3, route) | |
639 | } | |
640 | routed3 = true | |
641 | } | |
642 | ||
643 | router := New() | |
644 | router.SaveMatchedRoutePath = true | |
645 | router.Handle(http.MethodGet, route1, handle1) | |
646 | router.Handle(http.MethodGet, route2, handle2) | |
647 | router.Handle(http.MethodGet, route3, handle3) | |
648 | ||
649 | w := new(mockResponseWriter) | |
650 | r, _ := http.NewRequest(http.MethodGet, "/user/gopher", nil) | |
651 | router.ServeHTTP(w, r) | |
652 | if !routed1 || routed2 || routed3 { | |
653 | t.Fatal("Routing failed!") | |
654 | } | |
655 | ||
656 | w = new(mockResponseWriter) | |
657 | r, _ = http.NewRequest(http.MethodGet, "/user/gopher/details", nil) | |
658 | router.ServeHTTP(w, r) | |
659 | if !routed2 || routed3 { | |
660 | t.Fatal("Routing failed!") | |
661 | } | |
662 | ||
663 | w = new(mockResponseWriter) | |
664 | r, _ = http.NewRequest(http.MethodGet, "/", nil) | |
665 | router.ServeHTTP(w, r) | |
666 | if !routed3 { | |
667 | t.Fatal("Routing failed!") | |
668 | } | |
669 | } | |
670 | ||
574 | 671 | type mockFileSystem struct { |
575 | 672 | opened bool |
576 | 673 | } |
16 | 16 | return b |
17 | 17 | } |
18 | 18 | |
19 | const maxParamCount uint8 = ^uint8(0) | |
20 | ||
21 | func countParams(path string) uint8 { | |
19 | func longestCommonPrefix(a, b string) int { | |
20 | i := 0 | |
21 | max := min(len(a), len(b)) | |
22 | for i < max && a[i] == b[i] { | |
23 | i++ | |
24 | } | |
25 | return i | |
26 | } | |
27 | ||
28 | // Search for a wildcard segment and check the name for invalid characters. | |
29 | // Returns -1 as index, if no wildcard was found. | |
30 | func findWildcard(path string) (wilcard string, i int, valid bool) { | |
31 | // Find start | |
32 | for start, c := range []byte(path) { | |
33 | // A wildcard starts with ':' (param) or '*' (catch-all) | |
34 | if c != ':' && c != '*' { | |
35 | continue | |
36 | } | |
37 | ||
38 | // Find end and check for invalid characters | |
39 | valid = true | |
40 | for end, c := range []byte(path[start+1:]) { | |
41 | switch c { | |
42 | case '/': | |
43 | return path[start : start+1+end], start, valid | |
44 | case ':', '*': | |
45 | valid = false | |
46 | } | |
47 | } | |
48 | return path[start:], start, valid | |
49 | } | |
50 | return "", -1, false | |
51 | } | |
52 | ||
53 | func countParams(path string) uint16 { | |
22 | 54 | var n uint |
23 | for i := 0; i < len(path); i++ { | |
24 | if path[i] != ':' && path[i] != '*' { | |
25 | continue | |
26 | } | |
27 | n++ | |
28 | } | |
29 | if n >= uint(maxParamCount) { | |
30 | return maxParamCount | |
31 | } | |
32 | ||
33 | return uint8(n) | |
55 | for i := range []byte(path) { | |
56 | switch path[i] { | |
57 | case ':', '*': | |
58 | n++ | |
59 | } | |
60 | } | |
61 | return uint16(n) | |
34 | 62 | } |
35 | 63 | |
36 | 64 | type nodeType uint8 |
44 | 72 | |
45 | 73 | type node struct { |
46 | 74 | path string |
75 | indices string | |
47 | 76 | wildChild bool |
48 | 77 | nType nodeType |
49 | maxParams uint8 | |
50 | 78 | priority uint32 |
51 | indices string | |
52 | 79 | children []*node |
53 | 80 | handle Handle |
54 | 81 | } |
55 | 82 | |
56 | // increments priority of the given child and reorders if necessary | |
83 | // Increments priority of the given child and reorders if necessary | |
57 | 84 | func (n *node) incrementChildPrio(pos int) int { |
58 | n.children[pos].priority++ | |
59 | prio := n.children[pos].priority | |
60 | ||
61 | // adjust position (move to front) | |
85 | cs := n.children | |
86 | cs[pos].priority++ | |
87 | prio := cs[pos].priority | |
88 | ||
89 | // Adjust position (move to front) | |
62 | 90 | newPos := pos |
63 | for newPos > 0 && n.children[newPos-1].priority < prio { | |
64 | // swap node positions | |
65 | n.children[newPos-1], n.children[newPos] = n.children[newPos], n.children[newPos-1] | |
66 | ||
67 | newPos-- | |
68 | } | |
69 | ||
70 | // build new index char string | |
91 | for ; newPos > 0 && cs[newPos-1].priority < prio; newPos-- { | |
92 | // Swap node positions | |
93 | cs[newPos-1], cs[newPos] = cs[newPos], cs[newPos-1] | |
94 | } | |
95 | ||
96 | // Build new index char string | |
71 | 97 | if newPos != pos { |
72 | n.indices = n.indices[:newPos] + // unchanged prefix, might be empty | |
73 | n.indices[pos:pos+1] + // the index char we move | |
74 | n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos' | |
98 | n.indices = n.indices[:newPos] + // Unchanged prefix, might be empty | |
99 | n.indices[pos:pos+1] + // The index char we move | |
100 | n.indices[newPos:pos] + n.indices[pos+1:] // Rest without char at 'pos' | |
75 | 101 | } |
76 | 102 | |
77 | 103 | return newPos |
82 | 108 | func (n *node) addRoute(path string, handle Handle) { |
83 | 109 | fullPath := path |
84 | 110 | n.priority++ |
85 | numParams := countParams(path) | |
86 | ||
87 | // non-empty tree | |
88 | if len(n.path) > 0 || len(n.children) > 0 { | |
89 | walk: | |
90 | for { | |
91 | // Update maxParams of the current node | |
92 | if numParams > n.maxParams { | |
93 | n.maxParams = numParams | |
94 | } | |
95 | ||
96 | // Find the longest common prefix. | |
97 | // This also implies that the common prefix contains no ':' or '*' | |
98 | // since the existing key can't contain those chars. | |
99 | i := 0 | |
100 | max := min(len(path), len(n.path)) | |
101 | for i < max && path[i] == n.path[i] { | |
102 | i++ | |
103 | } | |
104 | ||
105 | // Split edge | |
106 | if i < len(n.path) { | |
107 | child := node{ | |
108 | path: n.path[i:], | |
109 | wildChild: n.wildChild, | |
110 | nType: static, | |
111 | indices: n.indices, | |
112 | children: n.children, | |
113 | handle: n.handle, | |
114 | priority: n.priority - 1, | |
115 | } | |
116 | ||
117 | // Update maxParams (max of all children) | |
118 | for i := range child.children { | |
119 | if child.children[i].maxParams > child.maxParams { | |
120 | child.maxParams = child.children[i].maxParams | |
121 | } | |
122 | } | |
123 | ||
124 | n.children = []*node{&child} | |
111 | ||
112 | // Empty tree | |
113 | if n.path == "" && n.indices == "" { | |
114 | n.insertChild(path, fullPath, handle) | |
115 | n.nType = root | |
116 | return | |
117 | } | |
118 | ||
119 | walk: | |
120 | for { | |
121 | // Find the longest common prefix. | |
122 | // This also implies that the common prefix contains no ':' or '*' | |
123 | // since the existing key can't contain those chars. | |
124 | i := longestCommonPrefix(path, n.path) | |
125 | ||
126 | // Split edge | |
127 | if i < len(n.path) { | |
128 | child := node{ | |
129 | path: n.path[i:], | |
130 | wildChild: n.wildChild, | |
131 | nType: static, | |
132 | indices: n.indices, | |
133 | children: n.children, | |
134 | handle: n.handle, | |
135 | priority: n.priority - 1, | |
136 | } | |
137 | ||
138 | n.children = []*node{&child} | |
139 | // []byte for proper unicode char conversion, see #65 | |
140 | n.indices = string([]byte{n.path[i]}) | |
141 | n.path = path[:i] | |
142 | n.handle = nil | |
143 | n.wildChild = false | |
144 | } | |
145 | ||
146 | // Make new node a child of this node | |
147 | if i < len(path) { | |
148 | path = path[i:] | |
149 | ||
150 | if n.wildChild { | |
151 | n = n.children[0] | |
152 | n.priority++ | |
153 | ||
154 | // Check if the wildcard matches | |
155 | if len(path) >= len(n.path) && n.path == path[:len(n.path)] && | |
156 | // Adding a child to a catchAll is not possible | |
157 | n.nType != catchAll && | |
158 | // Check for longer wildcard, e.g. :name and :names | |
159 | (len(n.path) >= len(path) || path[len(n.path)] == '/') { | |
160 | continue walk | |
161 | } else { | |
162 | // Wildcard conflict | |
163 | pathSeg := path | |
164 | if n.nType != catchAll { | |
165 | pathSeg = strings.SplitN(pathSeg, "/", 2)[0] | |
166 | } | |
167 | prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path | |
168 | panic("'" + pathSeg + | |
169 | "' in new path '" + fullPath + | |
170 | "' conflicts with existing wildcard '" + n.path + | |
171 | "' in existing prefix '" + prefix + | |
172 | "'") | |
173 | } | |
174 | } | |
175 | ||
176 | idxc := path[0] | |
177 | ||
178 | // '/' after param | |
179 | if n.nType == param && idxc == '/' && len(n.children) == 1 { | |
180 | n = n.children[0] | |
181 | n.priority++ | |
182 | continue walk | |
183 | } | |
184 | ||
185 | // Check if a child with the next path byte exists | |
186 | for i, c := range []byte(n.indices) { | |
187 | if c == idxc { | |
188 | i = n.incrementChildPrio(i) | |
189 | n = n.children[i] | |
190 | continue walk | |
191 | } | |
192 | } | |
193 | ||
194 | // Otherwise insert it | |
195 | if idxc != ':' && idxc != '*' { | |
125 | 196 | // []byte for proper unicode char conversion, see #65 |
126 | n.indices = string([]byte{n.path[i]}) | |
127 | n.path = path[:i] | |
128 | n.handle = nil | |
129 | n.wildChild = false | |
130 | } | |
131 | ||
132 | // Make new node a child of this node | |
133 | if i < len(path) { | |
134 | path = path[i:] | |
135 | ||
136 | if n.wildChild { | |
137 | n = n.children[0] | |
138 | n.priority++ | |
139 | ||
140 | // Update maxParams of the child node | |
141 | if numParams > n.maxParams { | |
142 | n.maxParams = numParams | |
143 | } | |
144 | numParams-- | |
145 | ||
146 | // Check if the wildcard matches | |
147 | if len(path) >= len(n.path) && n.path == path[:len(n.path)] && | |
148 | // Adding a child to a catchAll is not possible | |
149 | n.nType != catchAll && | |
150 | // Check for longer wildcard, e.g. :name and :names | |
151 | (len(n.path) >= len(path) || path[len(n.path)] == '/') { | |
152 | continue walk | |
153 | } else { | |
154 | // Wildcard conflict | |
155 | var pathSeg string | |
156 | if n.nType == catchAll { | |
157 | pathSeg = path | |
158 | } else { | |
159 | pathSeg = strings.SplitN(path, "/", 2)[0] | |
160 | } | |
161 | prefix := fullPath[:strings.Index(fullPath, pathSeg)] + n.path | |
162 | panic("'" + pathSeg + | |
163 | "' in new path '" + fullPath + | |
164 | "' conflicts with existing wildcard '" + n.path + | |
165 | "' in existing prefix '" + prefix + | |
166 | "'") | |
167 | } | |
168 | } | |
169 | ||
170 | c := path[0] | |
171 | ||
172 | // slash after param | |
173 | if n.nType == param && c == '/' && len(n.children) == 1 { | |
174 | n = n.children[0] | |
175 | n.priority++ | |
176 | continue walk | |
177 | } | |
178 | ||
179 | // Check if a child with the next path byte exists | |
180 | for i := 0; i < len(n.indices); i++ { | |
181 | if c == n.indices[i] { | |
182 | i = n.incrementChildPrio(i) | |
183 | n = n.children[i] | |
184 | continue walk | |
185 | } | |
186 | } | |
187 | ||
188 | // Otherwise insert it | |
189 | if c != ':' && c != '*' { | |
190 | // []byte for proper unicode char conversion, see #65 | |
191 | n.indices += string([]byte{c}) | |
192 | child := &node{ | |
193 | maxParams: numParams, | |
194 | } | |
195 | n.children = append(n.children, child) | |
196 | n.incrementChildPrio(len(n.indices) - 1) | |
197 | n = child | |
198 | } | |
199 | n.insertChild(numParams, path, fullPath, handle) | |
200 | return | |
201 | ||
202 | } else if i == len(path) { // Make node a (in-path) leaf | |
203 | if n.handle != nil { | |
204 | panic("a handle is already registered for path '" + fullPath + "'") | |
205 | } | |
206 | n.handle = handle | |
207 | } | |
197 | n.indices += string([]byte{idxc}) | |
198 | child := &node{} | |
199 | n.children = append(n.children, child) | |
200 | n.incrementChildPrio(len(n.indices) - 1) | |
201 | n = child | |
202 | } | |
203 | n.insertChild(path, fullPath, handle) | |
208 | 204 | return |
209 | 205 | } |
210 | } else { // Empty tree | |
211 | n.insertChild(numParams, path, fullPath, handle) | |
212 | n.nType = root | |
213 | } | |
214 | } | |
215 | ||
216 | func (n *node) insertChild(numParams uint8, path, fullPath string, handle Handle) { | |
217 | var offset int // already handled bytes of the path | |
218 | ||
219 | // find prefix until first wildcard (beginning with ':'' or '*'') | |
220 | for i, max := 0, len(path); numParams > 0; i++ { | |
221 | c := path[i] | |
222 | if c != ':' && c != '*' { | |
223 | continue | |
224 | } | |
225 | ||
226 | // find wildcard end (either '/' or path end) | |
227 | end := i + 1 | |
228 | for end < max && path[end] != '/' { | |
229 | switch path[end] { | |
230 | // the wildcard name must not contain ':' and '*' | |
231 | case ':', '*': | |
232 | panic("only one wildcard per path segment is allowed, has: '" + | |
233 | path[i:] + "' in path '" + fullPath + "'") | |
234 | default: | |
235 | end++ | |
236 | } | |
237 | } | |
238 | ||
239 | // check if this Node existing children which would be | |
206 | ||
207 | // Otherwise add handle to current node | |
208 | if n.handle != nil { | |
209 | panic("a handle is already registered for path '" + fullPath + "'") | |
210 | } | |
211 | n.handle = handle | |
212 | return | |
213 | } | |
214 | } | |
215 | ||
216 | func (n *node) insertChild(path, fullPath string, handle Handle) { | |
217 | for { | |
218 | // Find prefix until first wildcard | |
219 | wildcard, i, valid := findWildcard(path) | |
220 | if i < 0 { // No wilcard found | |
221 | break | |
222 | } | |
223 | ||
224 | // The wildcard name must not contain ':' and '*' | |
225 | if !valid { | |
226 | panic("only one wildcard per path segment is allowed, has: '" + | |
227 | wildcard + "' in path '" + fullPath + "'") | |
228 | } | |
229 | ||
230 | // Check if the wildcard has a name | |
231 | if len(wildcard) < 2 { | |
232 | panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") | |
233 | } | |
234 | ||
235 | // Check if this node has existing children which would be | |
240 | 236 | // unreachable if we insert the wildcard here |
241 | 237 | if len(n.children) > 0 { |
242 | panic("wildcard route '" + path[i:end] + | |
238 | panic("wildcard segment '" + wildcard + | |
243 | 239 | "' conflicts with existing children in path '" + fullPath + "'") |
244 | 240 | } |
245 | 241 | |
246 | // check if the wildcard has a name | |
247 | if end-i < 2 { | |
248 | panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") | |
249 | } | |
250 | ||
251 | if c == ':' { // param | |
252 | // split path at the beginning of the wildcard | |
242 | // param | |
243 | if wildcard[0] == ':' { | |
253 | 244 | if i > 0 { |
254 | n.path = path[offset:i] | |
255 | offset = i | |
256 | } | |
257 | ||
245 | // Insert prefix before the current wildcard | |
246 | n.path = path[:i] | |
247 | path = path[i:] | |
248 | } | |
249 | ||
250 | n.wildChild = true | |
258 | 251 | child := &node{ |
259 | nType: param, | |
260 | maxParams: numParams, | |
252 | nType: param, | |
253 | path: wildcard, | |
261 | 254 | } |
262 | 255 | n.children = []*node{child} |
263 | n.wildChild = true | |
264 | 256 | n = child |
265 | 257 | n.priority++ |
266 | numParams-- | |
267 | ||
268 | // if the path doesn't end with the wildcard, then there | |
258 | ||
259 | // If the path doesn't end with the wildcard, then there | |
269 | 260 | // will be another non-wildcard subpath starting with '/' |
270 | if end < max { | |
271 | n.path = path[offset:end] | |
272 | offset = end | |
273 | ||
261 | if len(wildcard) < len(path) { | |
262 | path = path[len(wildcard):] | |
274 | 263 | child := &node{ |
275 | maxParams: numParams, | |
276 | priority: 1, | |
264 | priority: 1, | |
277 | 265 | } |
278 | 266 | n.children = []*node{child} |
279 | 267 | n = child |
280 | } | |
281 | ||
282 | } else { // catchAll | |
283 | if end != max || numParams > 1 { | |
284 | panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") | |
285 | } | |
286 | ||
287 | if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { | |
288 | panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") | |
289 | } | |
290 | ||
291 | // currently fixed width 1 for '/' | |
292 | i-- | |
293 | if path[i] != '/' { | |
294 | panic("no / before catch-all in path '" + fullPath + "'") | |
295 | } | |
296 | ||
297 | n.path = path[offset:i] | |
298 | ||
299 | // first node: catchAll node with empty path | |
300 | child := &node{ | |
301 | wildChild: true, | |
302 | nType: catchAll, | |
303 | maxParams: 1, | |
304 | } | |
305 | // update maxParams of the parent node | |
306 | if n.maxParams < 1 { | |
307 | n.maxParams = 1 | |
308 | } | |
309 | n.children = []*node{child} | |
310 | n.indices = string(path[i]) | |
311 | n = child | |
312 | n.priority++ | |
313 | ||
314 | // second node: node holding the variable | |
315 | child = &node{ | |
316 | path: path[i:], | |
317 | nType: catchAll, | |
318 | maxParams: 1, | |
319 | handle: handle, | |
320 | priority: 1, | |
321 | } | |
322 | n.children = []*node{child} | |
323 | ||
268 | continue | |
269 | } | |
270 | ||
271 | // Otherwise we're done. Insert the handle in the new leaf | |
272 | n.handle = handle | |
324 | 273 | return |
325 | 274 | } |
326 | } | |
327 | ||
328 | // insert remaining path part and handle to the leaf | |
329 | n.path = path[offset:] | |
275 | ||
276 | // catchAll | |
277 | if i+len(wildcard) != len(path) { | |
278 | panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") | |
279 | } | |
280 | ||
281 | if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { | |
282 | panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") | |
283 | } | |
284 | ||
285 | // Currently fixed width 1 for '/' | |
286 | i-- | |
287 | if path[i] != '/' { | |
288 | panic("no / before catch-all in path '" + fullPath + "'") | |
289 | } | |
290 | ||
291 | n.path = path[:i] | |
292 | ||
293 | // First node: catchAll node with empty path | |
294 | child := &node{ | |
295 | wildChild: true, | |
296 | nType: catchAll, | |
297 | } | |
298 | n.children = []*node{child} | |
299 | n.indices = string('/') | |
300 | n = child | |
301 | n.priority++ | |
302 | ||
303 | // Second node: node holding the variable | |
304 | child = &node{ | |
305 | path: path[i:], | |
306 | nType: catchAll, | |
307 | handle: handle, | |
308 | priority: 1, | |
309 | } | |
310 | n.children = []*node{child} | |
311 | ||
312 | return | |
313 | } | |
314 | ||
315 | // If no wildcard was found, simply insert the path and handle | |
316 | n.path = path | |
330 | 317 | n.handle = handle |
331 | 318 | } |
332 | 319 | |
335 | 322 | // If no handle can be found, a TSR (trailing slash redirect) recommendation is |
336 | 323 | // made if a handle exists with an extra (without the) trailing slash for the |
337 | 324 | // given path. |
338 | func (n *node) getValue(path string) (handle Handle, p Params, tsr bool) { | |
339 | walk: // outer loop for walking the tree | |
325 | func (n *node) getValue(path string, params func() *Params) (handle Handle, ps *Params, tsr bool) { | |
326 | walk: // Outer loop for walking the tree | |
340 | 327 | for { |
341 | if len(path) > len(n.path) { | |
342 | if path[:len(n.path)] == n.path { | |
343 | path = path[len(n.path):] | |
328 | prefix := n.path | |
329 | if len(path) > len(prefix) { | |
330 | if path[:len(prefix)] == prefix { | |
331 | path = path[len(prefix):] | |
332 | ||
344 | 333 | // If this node does not have a wildcard (param or catchAll) |
345 | // child, we can just look up the next child node and continue | |
334 | // child, we can just look up the next child node and continue | |
346 | 335 | // to walk down the tree |
347 | 336 | if !n.wildChild { |
348 | c := path[0] | |
349 | for i := 0; i < len(n.indices); i++ { | |
350 | if c == n.indices[i] { | |
337 | idxc := path[0] | |
338 | for i, c := range []byte(n.indices) { | |
339 | if c == idxc { | |
351 | 340 | n = n.children[i] |
352 | 341 | continue walk |
353 | 342 | } |
358 | 347 | // trailing slash if a leaf exists for that path. |
359 | 348 | tsr = (path == "/" && n.handle != nil) |
360 | 349 | return |
361 | ||
362 | } | |
363 | ||
364 | // handle wildcard child | |
350 | } | |
351 | ||
352 | // Handle wildcard child | |
365 | 353 | n = n.children[0] |
366 | 354 | switch n.nType { |
367 | 355 | case param: |
368 | // find param end (either '/' or path end) | |
356 | // Find param end (either '/' or path end) | |
369 | 357 | end := 0 |
370 | 358 | for end < len(path) && path[end] != '/' { |
371 | 359 | end++ |
372 | 360 | } |
373 | 361 | |
374 | // save param value | |
375 | if p == nil { | |
376 | // lazy allocation | |
377 | p = make(Params, 0, n.maxParams) | |
378 | } | |
379 | i := len(p) | |
380 | p = p[:i+1] // expand slice within preallocated capacity | |
381 | p[i].Key = n.path[1:] | |
382 | p[i].Value = path[:end] | |
383 | ||
384 | // we need to go deeper! | |
362 | // Save param value | |
363 | if params != nil { | |
364 | if ps == nil { | |
365 | ps = params() | |
366 | } | |
367 | // Expand slice within preallocated capacity | |
368 | i := len(*ps) | |
369 | *ps = (*ps)[:i+1] | |
370 | (*ps)[i] = Param{ | |
371 | Key: n.path[1:], | |
372 | Value: path[:end], | |
373 | } | |
374 | } | |
375 | ||
376 | // We need to go deeper! | |
385 | 377 | if end < len(path) { |
386 | 378 | if len(n.children) > 0 { |
387 | 379 | path = path[end:] |
400 | 392 | // No handle found. Check if a handle for this path + a |
401 | 393 | // trailing slash exists for TSR recommendation |
402 | 394 | n = n.children[0] |
403 | tsr = (n.path == "/" && n.handle != nil) | |
395 | tsr = (n.path == "/" && n.handle != nil) || (n.path == "" && n.indices == "/") | |
404 | 396 | } |
405 | 397 | |
406 | 398 | return |
407 | 399 | |
408 | 400 | case catchAll: |
409 | // save param value | |
410 | if p == nil { | |
411 | // lazy allocation | |
412 | p = make(Params, 0, n.maxParams) | |
413 | } | |
414 | i := len(p) | |
415 | p = p[:i+1] // expand slice within preallocated capacity | |
416 | p[i].Key = n.path[2:] | |
417 | p[i].Value = path | |
401 | // Save param value | |
402 | if params != nil { | |
403 | if ps == nil { | |
404 | ps = params() | |
405 | } | |
406 | // Expand slice within preallocated capacity | |
407 | i := len(*ps) | |
408 | *ps = (*ps)[:i+1] | |
409 | (*ps)[i] = Param{ | |
410 | Key: n.path[2:], | |
411 | Value: path, | |
412 | } | |
413 | } | |
418 | 414 | |
419 | 415 | handle = n.handle |
420 | 416 | return |
423 | 419 | panic("invalid node type") |
424 | 420 | } |
425 | 421 | } |
426 | } else if path == n.path { | |
422 | } else if path == prefix { | |
427 | 423 | // We should have reached the node containing the handle. |
428 | 424 | // Check if this node has a handle registered. |
429 | 425 | if handle = n.handle; handle != nil { |
430 | 426 | return |
431 | 427 | } |
432 | 428 | |
429 | // If there is no handle for this route, but this route has a | |
430 | // wildcard child, there must be a handle for this path with an | |
431 | // additional trailing slash | |
433 | 432 | if path == "/" && n.wildChild && n.nType != root { |
434 | 433 | tsr = true |
435 | 434 | return |
437 | 436 | |
438 | 437 | // No handle found. Check if a handle for this path + a |
439 | 438 | // trailing slash exists for trailing slash recommendation |
440 | for i := 0; i < len(n.indices); i++ { | |
441 | if n.indices[i] == '/' { | |
439 | for i, c := range []byte(n.indices) { | |
440 | if c == '/' { | |
442 | 441 | n = n.children[i] |
443 | 442 | tsr = (len(n.path) == 1 && n.handle != nil) || |
444 | 443 | (n.nType == catchAll && n.children[0].handle != nil) |
445 | 444 | return |
446 | 445 | } |
447 | 446 | } |
448 | ||
449 | 447 | return |
450 | 448 | } |
451 | 449 | |
452 | 450 | // Nothing found. We can recommend to redirect to the same URL with an |
453 | 451 | // extra trailing slash if a leaf exists for that path |
454 | 452 | tsr = (path == "/") || |
455 | (len(n.path) == len(path)+1 && n.path[len(path)] == '/' && | |
456 | path == n.path[:len(n.path)-1] && n.handle != nil) | |
453 | (len(prefix) == len(path)+1 && prefix[len(path)] == '/' && | |
454 | path == prefix[:len(prefix)-1] && n.handle != nil) | |
457 | 455 | return |
458 | 456 | } |
459 | 457 | } |
462 | 460 | // It can optionally also fix trailing slashes. |
463 | 461 | // It returns the case-corrected path and a bool indicating whether the lookup |
464 | 462 | // was successful. |
465 | func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) { | |
466 | return n.findCaseInsensitivePathRec( | |
463 | func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (fixedPath string, found bool) { | |
464 | const stackBufSize = 128 | |
465 | ||
466 | // Use a static sized buffer on the stack in the common case. | |
467 | // If the path is too long, allocate a buffer on the heap instead. | |
468 | buf := make([]byte, 0, stackBufSize) | |
469 | if l := len(path) + 1; l > stackBufSize { | |
470 | buf = make([]byte, 0, l) | |
471 | } | |
472 | ||
473 | ciPath := n.findCaseInsensitivePathRec( | |
467 | 474 | path, |
468 | make([]byte, 0, len(path)+1), // preallocate enough memory for new path | |
469 | [4]byte{}, // empty rune buffer | |
475 | buf, // Preallocate enough memory for new path | |
476 | [4]byte{}, // Empty rune buffer | |
470 | 477 | fixTrailingSlash, |
471 | 478 | ) |
472 | } | |
473 | ||
474 | // shift bytes in array by n bytes left | |
479 | ||
480 | return string(ciPath), ciPath != nil | |
481 | } | |
482 | ||
483 | // Shift bytes in array by n bytes left | |
475 | 484 | func shiftNRuneBytes(rb [4]byte, n int) [4]byte { |
476 | 485 | switch n { |
477 | 486 | case 0: |
487 | 496 | } |
488 | 497 | } |
489 | 498 | |
490 | // recursive case-insensitive lookup function used by n.findCaseInsensitivePath | |
491 | func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) ([]byte, bool) { | |
499 | // Recursive case-insensitive lookup function used by n.findCaseInsensitivePath | |
500 | func (n *node) findCaseInsensitivePathRec(path string, ciPath []byte, rb [4]byte, fixTrailingSlash bool) []byte { | |
492 | 501 | npLen := len(n.path) |
493 | 502 | |
494 | walk: // outer loop for walking the tree | |
503 | walk: // Outer loop for walking the tree | |
495 | 504 | for len(path) >= npLen && (npLen == 0 || strings.EqualFold(path[1:npLen], n.path[1:])) { |
496 | // add common prefix to result | |
497 | ||
505 | // Add common prefix to result | |
498 | 506 | oldPath := path |
499 | 507 | path = path[npLen:] |
500 | 508 | ciPath = append(ciPath, n.path...) |
504 | 512 | // we can just look up the next child node and continue to walk down |
505 | 513 | // the tree |
506 | 514 | if !n.wildChild { |
507 | // skip rune bytes already processed | |
515 | // Skip rune bytes already processed | |
508 | 516 | rb = shiftNRuneBytes(rb, npLen) |
509 | 517 | |
510 | 518 | if rb[0] != 0 { |
511 | // old rune not finished | |
512 | for i := 0; i < len(n.indices); i++ { | |
513 | if n.indices[i] == rb[0] { | |
519 | // Old rune not finished | |
520 | idxc := rb[0] | |
521 | for i, c := range []byte(n.indices) { | |
522 | if c == idxc { | |
514 | 523 | // continue with child node |
515 | 524 | n = n.children[i] |
516 | 525 | npLen = len(n.path) |
518 | 527 | } |
519 | 528 | } |
520 | 529 | } else { |
521 | // process a new rune | |
530 | // Process a new rune | |
522 | 531 | var rv rune |
523 | 532 | |
524 | // find rune start | |
525 | // runes are up to 4 byte long, | |
526 | // -4 would definitely be another rune | |
533 | // Find rune start. | |
534 | // Runes are up to 4 byte long, | |
535 | // -4 would definitely be another rune. | |
527 | 536 | var off int |
528 | 537 | for max := min(npLen, 3); off < max; off++ { |
529 | 538 | if i := npLen - off; utf8.RuneStart(oldPath[i]) { |
533 | 542 | } |
534 | 543 | } |
535 | 544 | |
536 | // calculate lowercase bytes of current rune | |
545 | // Calculate lowercase bytes of current rune | |
537 | 546 | lo := unicode.ToLower(rv) |
538 | 547 | utf8.EncodeRune(rb[:], lo) |
539 | 548 | |
540 | // skip already processed bytes | |
549 | // Skip already processed bytes | |
541 | 550 | rb = shiftNRuneBytes(rb, off) |
542 | 551 | |
543 | for i := 0; i < len(n.indices); i++ { | |
544 | // lowercase matches | |
545 | if n.indices[i] == rb[0] { | |
552 | idxc := rb[0] | |
553 | for i, c := range []byte(n.indices) { | |
554 | // Lowercase matches | |
555 | if c == idxc { | |
546 | 556 | // must use a recursive approach since both the |
547 | 557 | // uppercase byte and the lowercase byte might exist |
548 | 558 | // as an index |
549 | if out, found := n.children[i].findCaseInsensitivePathRec( | |
559 | if out := n.children[i].findCaseInsensitivePathRec( | |
550 | 560 | path, ciPath, rb, fixTrailingSlash, |
551 | ); found { | |
552 | return out, true | |
561 | ); out != nil { | |
562 | return out | |
553 | 563 | } |
554 | 564 | break |
555 | 565 | } |
556 | 566 | } |
557 | 567 | |
558 | // if we found no match, the same for the uppercase rune, | |
568 | // If we found no match, the same for the uppercase rune, | |
559 | 569 | // if it differs |
560 | 570 | if up := unicode.ToUpper(rv); up != lo { |
561 | 571 | utf8.EncodeRune(rb[:], up) |
562 | 572 | rb = shiftNRuneBytes(rb, off) |
563 | 573 | |
564 | for i, c := 0, rb[0]; i < len(n.indices); i++ { | |
565 | // uppercase matches | |
566 | if n.indices[i] == c { | |
567 | // continue with child node | |
574 | idxc := rb[0] | |
575 | for i, c := range []byte(n.indices) { | |
576 | // Uppercase matches | |
577 | if c == idxc { | |
578 | // Continue with child node | |
568 | 579 | n = n.children[i] |
569 | 580 | npLen = len(n.path) |
570 | 581 | continue walk |
575 | 586 | |
576 | 587 | // Nothing found. We can recommend to redirect to the same URL |
577 | 588 | // without a trailing slash if a leaf exists for that path |
578 | return ciPath, (fixTrailingSlash && path == "/" && n.handle != nil) | |
589 | if fixTrailingSlash && path == "/" && n.handle != nil { | |
590 | return ciPath | |
591 | } | |
592 | return nil | |
579 | 593 | } |
580 | 594 | |
581 | 595 | n = n.children[0] |
582 | 596 | switch n.nType { |
583 | 597 | case param: |
584 | // find param end (either '/' or path end) | |
585 | k := 0 | |
586 | for k < len(path) && path[k] != '/' { | |
587 | k++ | |
588 | } | |
589 | ||
590 | // add param value to case insensitive path | |
591 | ciPath = append(ciPath, path[:k]...) | |
592 | ||
593 | // we need to go deeper! | |
594 | if k < len(path) { | |
598 | // Find param end (either '/' or path end) | |
599 | end := 0 | |
600 | for end < len(path) && path[end] != '/' { | |
601 | end++ | |
602 | } | |
603 | ||
604 | // Add param value to case insensitive path | |
605 | ciPath = append(ciPath, path[:end]...) | |
606 | ||
607 | // We need to go deeper! | |
608 | if end < len(path) { | |
595 | 609 | if len(n.children) > 0 { |
596 | // continue with child node | |
610 | // Continue with child node | |
597 | 611 | n = n.children[0] |
598 | 612 | npLen = len(n.path) |
599 | path = path[k:] | |
613 | path = path[end:] | |
600 | 614 | continue |
601 | 615 | } |
602 | 616 | |
603 | 617 | // ... but we can't |
604 | if fixTrailingSlash && len(path) == k+1 { | |
605 | return ciPath, true | |
606 | } | |
607 | return ciPath, false | |
618 | if fixTrailingSlash && len(path) == end+1 { | |
619 | return ciPath | |
620 | } | |
621 | return nil | |
608 | 622 | } |
609 | 623 | |
610 | 624 | if n.handle != nil { |
611 | return ciPath, true | |
625 | return ciPath | |
612 | 626 | } else if fixTrailingSlash && len(n.children) == 1 { |
613 | 627 | // No handle found. Check if a handle for this path + a |
614 | 628 | // trailing slash exists |
615 | 629 | n = n.children[0] |
616 | 630 | if n.path == "/" && n.handle != nil { |
617 | return append(ciPath, '/'), true | |
618 | } | |
619 | } | |
620 | return ciPath, false | |
631 | return append(ciPath, '/') | |
632 | } | |
633 | } | |
634 | return nil | |
621 | 635 | |
622 | 636 | case catchAll: |
623 | return append(ciPath, path...), true | |
637 | return append(ciPath, path...) | |
624 | 638 | |
625 | 639 | default: |
626 | 640 | panic("invalid node type") |
629 | 643 | // We should have reached the node containing the handle. |
630 | 644 | // Check if this node has a handle registered. |
631 | 645 | if n.handle != nil { |
632 | return ciPath, true | |
646 | return ciPath | |
633 | 647 | } |
634 | 648 | |
635 | 649 | // No handle found. |
636 | 650 | // Try to fix the path by adding a trailing slash |
637 | 651 | if fixTrailingSlash { |
638 | for i := 0; i < len(n.indices); i++ { | |
639 | if n.indices[i] == '/' { | |
652 | for i, c := range []byte(n.indices) { | |
653 | if c == '/' { | |
640 | 654 | n = n.children[i] |
641 | 655 | if (len(n.path) == 1 && n.handle != nil) || |
642 | 656 | (n.nType == catchAll && n.children[0].handle != nil) { |
643 | return append(ciPath, '/'), true | |
644 | } | |
645 | return ciPath, false | |
646 | } | |
647 | } | |
648 | } | |
649 | return ciPath, false | |
657 | return append(ciPath, '/') | |
658 | } | |
659 | return nil | |
660 | } | |
661 | } | |
662 | } | |
663 | return nil | |
650 | 664 | } |
651 | 665 | } |
652 | 666 | |
654 | 668 | // Try to fix the path by adding / removing a trailing slash |
655 | 669 | if fixTrailingSlash { |
656 | 670 | if path == "/" { |
657 | return ciPath, true | |
671 | return ciPath | |
658 | 672 | } |
659 | 673 | if len(path)+1 == npLen && n.path[len(path)] == '/' && |
660 | 674 | strings.EqualFold(path[1:], n.path[1:len(path)]) && n.handle != nil { |
661 | return append(ciPath, n.path...), true | |
662 | } | |
663 | } | |
664 | return ciPath, false | |
665 | } | |
675 | return append(ciPath, n.path...) | |
676 | } | |
677 | } | |
678 | return nil | |
679 | } |
12 | 12 | "testing" |
13 | 13 | ) |
14 | 14 | |
15 | func printChildren(n *node, prefix string) { | |
16 | fmt.Printf(" %02d:%02d %s%s[%d] %v %t %d \r\n", n.priority, n.maxParams, prefix, n.path, len(n.children), n.handle, n.wildChild, n.nType) | |
17 | for l := len(n.path); l > 0; l-- { | |
18 | prefix += " " | |
19 | } | |
20 | for _, child := range n.children { | |
21 | printChildren(child, prefix) | |
22 | } | |
23 | } | |
15 | // func printChildren(n *node, prefix string) { | |
16 | // fmt.Printf(" %02d %s%s[%d] %v %t %d \r\n", n.priority, prefix, n.path, len(n.children), n.handle, n.wildChild, n.nType) | |
17 | // for l := len(n.path); l > 0; l-- { | |
18 | // prefix += " " | |
19 | // } | |
20 | // for _, child := range n.children { | |
21 | // printChildren(child, prefix) | |
22 | // } | |
23 | // } | |
24 | 24 | |
25 | 25 | // Used as a workaround since we can't compare functions or their addresses |
26 | 26 | var fakeHandlerValue string |
38 | 38 | ps Params |
39 | 39 | } |
40 | 40 | |
41 | func getParams() *Params { | |
42 | ps := make(Params, 0, 20) | |
43 | return &ps | |
44 | } | |
45 | ||
41 | 46 | func checkRequests(t *testing.T, tree *node, requests testRequests) { |
42 | 47 | for _, request := range requests { |
43 | handler, ps, _ := tree.getValue(request.path) | |
44 | ||
45 | if handler == nil { | |
48 | handler, psp, _ := tree.getValue(request.path, getParams) | |
49 | ||
50 | switch { | |
51 | case handler == nil: | |
46 | 52 | if !request.nilHandler { |
47 | 53 | t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path) |
48 | 54 | } |
49 | } else if request.nilHandler { | |
55 | case request.nilHandler: | |
50 | 56 | t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path) |
51 | } else { | |
57 | default: | |
52 | 58 | handler(nil, nil, nil) |
53 | 59 | if fakeHandlerValue != request.route { |
54 | 60 | t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route) |
55 | 61 | } |
62 | } | |
63 | ||
64 | var ps Params | |
65 | if psp != nil { | |
66 | ps = *psp | |
56 | 67 | } |
57 | 68 | |
58 | 69 | if !reflect.DeepEqual(ps, request.ps) { |
81 | 92 | return prio |
82 | 93 | } |
83 | 94 | |
84 | func checkMaxParams(t *testing.T, n *node) uint8 { | |
85 | var maxParams uint8 | |
86 | for i := range n.children { | |
87 | params := checkMaxParams(t, n.children[i]) | |
88 | if params > maxParams { | |
89 | maxParams = params | |
90 | } | |
91 | } | |
92 | if n.nType > root && !n.wildChild { | |
93 | maxParams++ | |
94 | } | |
95 | ||
96 | if n.maxParams != maxParams { | |
97 | t.Errorf( | |
98 | "maxParams mismatch for node '%s': is %d, should be %d", | |
99 | n.path, n.maxParams, maxParams, | |
100 | ) | |
101 | } | |
102 | ||
103 | return maxParams | |
104 | } | |
105 | ||
106 | 95 | func TestCountParams(t *testing.T) { |
107 | 96 | if countParams("/path/:param1/static/*catch-all") != 2 { |
108 | 97 | t.Fail() |
109 | 98 | } |
110 | if countParams(strings.Repeat("/:param", 256)) != 255 { | |
99 | if countParams(strings.Repeat("/:param", 256)) != 256 { | |
111 | 100 | t.Fail() |
112 | 101 | } |
113 | 102 | } |
132 | 121 | tree.addRoute(route, fakeHandler(route)) |
133 | 122 | } |
134 | 123 | |
135 | //printChildren(tree, "") | |
124 | // printChildren(tree, "") | |
136 | 125 | |
137 | 126 | checkRequests(t, tree, testRequests{ |
138 | 127 | {"/a", false, "/a", nil}, |
149 | 138 | }) |
150 | 139 | |
151 | 140 | checkPriorities(t, tree) |
152 | checkMaxParams(t, tree) | |
153 | 141 | } |
154 | 142 | |
155 | 143 | func TestTreeWildcard(t *testing.T) { |
175 | 163 | tree.addRoute(route, fakeHandler(route)) |
176 | 164 | } |
177 | 165 | |
178 | //printChildren(tree, "") | |
166 | // printChildren(tree, "") | |
179 | 167 | |
180 | 168 | checkRequests(t, tree, testRequests{ |
181 | 169 | {"/", false, "/", nil}, |
195 | 183 | }) |
196 | 184 | |
197 | 185 | checkPriorities(t, tree) |
198 | checkMaxParams(t, tree) | |
199 | 186 | } |
200 | 187 | |
201 | 188 | func catchPanic(testFunc func()) (recv interface{}) { |
215 | 202 | func testRoutes(t *testing.T, routes []testRoute) { |
216 | 203 | tree := &node{} |
217 | 204 | |
218 | for _, route := range routes { | |
205 | for i := range routes { | |
206 | route := routes[i] | |
219 | 207 | recv := catchPanic(func() { |
220 | 208 | tree.addRoute(route.path, nil) |
221 | 209 | }) |
229 | 217 | } |
230 | 218 | } |
231 | 219 | |
232 | //printChildren(tree, "") | |
220 | // printChildren(tree, "") | |
233 | 221 | } |
234 | 222 | |
235 | 223 | func TestTreeWildcardConflict(t *testing.T) { |
279 | 267 | "/search/:query", |
280 | 268 | "/user_:name", |
281 | 269 | } |
282 | for _, route := range routes { | |
270 | for i := range routes { | |
271 | route := routes[i] | |
283 | 272 | recv := catchPanic(func() { |
284 | 273 | tree.addRoute(route, fakeHandler(route)) |
285 | 274 | }) |
296 | 285 | } |
297 | 286 | } |
298 | 287 | |
299 | //printChildren(tree, "") | |
288 | // printChildren(tree, "") | |
300 | 289 | |
301 | 290 | checkRequests(t, tree, testRequests{ |
302 | 291 | {"/", false, "/", nil}, |
316 | 305 | "/cmd/:/", |
317 | 306 | "/src/*", |
318 | 307 | } |
319 | for _, route := range routes { | |
308 | for i := range routes { | |
309 | route := routes[i] | |
320 | 310 | recv := catchPanic(func() { |
321 | 311 | tree.addRoute(route, nil) |
322 | 312 | }) |
349 | 339 | tree := &node{} |
350 | 340 | var route = "/cmd/*filepath" |
351 | 341 | tree.addRoute(route, fakeHandler(route)) |
352 | checkMaxParams(t, tree) | |
353 | 342 | } |
354 | 343 | |
355 | 344 | func TestTreeDoubleWildcard(t *testing.T) { |
361 | 350 | "/:foo*bar", |
362 | 351 | } |
363 | 352 | |
364 | for _, route := range routes { | |
353 | for i := range routes { | |
354 | route := routes[i] | |
365 | 355 | tree := &node{} |
366 | 356 | recv := catchPanic(func() { |
367 | 357 | tree.addRoute(route, nil) |
372 | 362 | } |
373 | 363 | } |
374 | 364 | } |
375 | ||
376 | /*func TestTreeDuplicateWildcard(t *testing.T) { | |
377 | tree := &node{} | |
378 | ||
379 | routes := [...]string{ | |
380 | "/:id/:name/:id", | |
381 | } | |
382 | for _, route := range routes { | |
383 | ... | |
384 | } | |
385 | }*/ | |
386 | 365 | |
387 | 366 | func TestTreeTrailingSlashRedirect(t *testing.T) { |
388 | 367 | tree := &node{} |
412 | 391 | "/no/a", |
413 | 392 | "/no/b", |
414 | 393 | "/api/hello/:name", |
415 | } | |
416 | for _, route := range routes { | |
394 | "/vendor/:x/*y", | |
395 | } | |
396 | for i := range routes { | |
397 | route := routes[i] | |
417 | 398 | recv := catchPanic(func() { |
418 | 399 | tree.addRoute(route, fakeHandler(route)) |
419 | 400 | }) |
422 | 403 | } |
423 | 404 | } |
424 | 405 | |
425 | //printChildren(tree, "") | |
406 | // printChildren(tree, "") | |
426 | 407 | |
427 | 408 | tsrRoutes := [...]string{ |
428 | 409 | "/hi/", |
439 | 420 | "/admin/config/", |
440 | 421 | "/admin/config/permissions/", |
441 | 422 | "/doc/", |
423 | "/vendor/x", | |
442 | 424 | } |
443 | 425 | for _, route := range tsrRoutes { |
444 | handler, _, tsr := tree.getValue(route) | |
426 | handler, _, tsr := tree.getValue(route, nil) | |
445 | 427 | if handler != nil { |
446 | 428 | t.Fatalf("non-nil handler for TSR route '%s", route) |
447 | 429 | } else if !tsr { |
458 | 440 | "/api/world/abc", |
459 | 441 | } |
460 | 442 | for _, route := range noTsrRoutes { |
461 | handler, _, tsr := tree.getValue(route) | |
443 | handler, _, tsr := tree.getValue(route, nil) | |
462 | 444 | if handler != nil { |
463 | 445 | t.Fatalf("non-nil handler for No-TSR route '%s", route) |
464 | 446 | } else if tsr { |
477 | 459 | t.Fatalf("panic inserting test route: %v", recv) |
478 | 460 | } |
479 | 461 | |
480 | handler, _, tsr := tree.getValue("/") | |
462 | handler, _, tsr := tree.getValue("/", nil) | |
481 | 463 | if handler != nil { |
482 | 464 | t.Fatalf("non-nil handler") |
483 | 465 | } else if tsr { |
487 | 469 | |
488 | 470 | func TestTreeFindCaseInsensitivePath(t *testing.T) { |
489 | 471 | tree := &node{} |
472 | ||
473 | longPath := "/l" + strings.Repeat("o", 128) + "ng" | |
474 | lOngPath := "/l" + strings.Repeat("O", 128) + "ng/" | |
490 | 475 | |
491 | 476 | routes := [...]string{ |
492 | 477 | "/hi", |
521 | 506 | "/w/♭/", // 3 byte, last byte differs |
522 | 507 | "/w/𠜎", // 4 byte |
523 | 508 | "/w/𠜏/", // 4 byte |
524 | } | |
525 | ||
526 | for _, route := range routes { | |
509 | longPath, | |
510 | } | |
511 | ||
512 | for i := range routes { | |
513 | route := routes[i] | |
527 | 514 | recv := catchPanic(func() { |
528 | 515 | tree.addRoute(route, fakeHandler(route)) |
529 | 516 | }) |
534 | 521 | |
535 | 522 | // Check out == in for all registered routes |
536 | 523 | // With fixTrailingSlash = true |
537 | for _, route := range routes { | |
524 | for i := range routes { | |
525 | route := routes[i] | |
538 | 526 | out, found := tree.findCaseInsensitivePath(route, true) |
539 | 527 | if !found { |
540 | 528 | t.Errorf("Route '%s' not found!", route) |
541 | } else if string(out) != route { | |
542 | t.Errorf("Wrong result for route '%s': %s", route, string(out)) | |
529 | } else if out != route { | |
530 | t.Errorf("Wrong result for route '%s': %s", route, out) | |
543 | 531 | } |
544 | 532 | } |
545 | 533 | // With fixTrailingSlash = false |
546 | for _, route := range routes { | |
534 | for i := range routes { | |
535 | route := routes[i] | |
547 | 536 | out, found := tree.findCaseInsensitivePath(route, false) |
548 | 537 | if !found { |
549 | 538 | t.Errorf("Route '%s' not found!", route) |
550 | } else if string(out) != route { | |
551 | t.Errorf("Wrong result for route '%s': %s", route, string(out)) | |
539 | } else if out != route { | |
540 | t.Errorf("Wrong result for route '%s': %s", route, out) | |
552 | 541 | } |
553 | 542 | } |
554 | 543 | |
613 | 602 | {"/w/♭", "/w/♭/", true, true}, |
614 | 603 | {"/w/𠜎/", "/w/𠜎", true, true}, |
615 | 604 | {"/w/𠜏", "/w/𠜏/", true, true}, |
605 | {lOngPath, longPath, true, true}, | |
616 | 606 | } |
617 | 607 | // With fixTrailingSlash = true |
618 | 608 | for _, test := range tests { |
619 | 609 | out, found := tree.findCaseInsensitivePath(test.in, true) |
620 | if found != test.found || (found && (string(out) != test.out)) { | |
610 | if found != test.found || (found && (out != test.out)) { | |
621 | 611 | t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", |
622 | test.in, string(out), found, test.out, test.found) | |
612 | test.in, out, found, test.out, test.found) | |
623 | 613 | return |
624 | 614 | } |
625 | 615 | } |
628 | 618 | out, found := tree.findCaseInsensitivePath(test.in, false) |
629 | 619 | if test.slash { |
630 | 620 | if found { // test needs a trailingSlash fix. It must not be found! |
631 | t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, string(out)) | |
621 | t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, out) | |
632 | 622 | } |
633 | 623 | } else { |
634 | if found != test.found || (found && (string(out) != test.out)) { | |
624 | if found != test.found || (found && (out != test.out)) { | |
635 | 625 | t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", |
636 | test.in, string(out), found, test.out, test.found) | |
626 | test.in, out, found, test.out, test.found) | |
637 | 627 | return |
638 | 628 | } |
639 | 629 | } |
652 | 642 | |
653 | 643 | // normal lookup |
654 | 644 | recv := catchPanic(func() { |
655 | tree.getValue("/test") | |
645 | tree.getValue("/test", nil) | |
656 | 646 | }) |
657 | 647 | if rs, ok := recv.(string); !ok || rs != panicMsg { |
658 | 648 | t.Fatalf("Expected panic '"+panicMsg+"', got '%v'", recv) |
681 | 671 | {"/conooo/xxx", "ooo", `/con:tact`, `:tact`}, |
682 | 672 | } |
683 | 673 | |
684 | for _, conflict := range conflicts { | |
674 | for i := range conflicts { | |
675 | conflict := conflicts[i] | |
676 | ||
685 | 677 | // I have to re-create a 'tree', because the 'tree' will be |
686 | 678 | // in an inconsistent state when the loop recovers from the |
687 | 679 | // panic which threw by 'addRoute' function. |
692 | 684 | "/who/foo/hello", |
693 | 685 | } |
694 | 686 | |
695 | for _, route := range routes { | |
687 | for i := range routes { | |
688 | route := routes[i] | |
696 | 689 | tree.addRoute(route, fakeHandler(route)) |
697 | 690 | } |
698 | 691 |