1 /**
2 	Pattern based URL router for HTTP request.
3 
4 	See `URLRouter` for more details.
5 
6 	Copyright: © 2012-2015 Sönke Ludwig
7 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
8 	Authors: Sönke Ludwig
9 */
10 module vibe.http.router;
11 
12 public import vibe.http.server;
13 
14 import vibe.core.log;
15 
16 import std.functional;
17 
18 
19 /**
20 	Routes HTTP requests based on the request method and URL.
21 
22 	Routes are matched using a special URL match string that supports two forms
23 	of placeholders. See the sections below for more details.
24 
25 	Registered routes are matched according to the same sequence as initially
26 	specified using `match`, `get`, `post` etc. Matching ends as soon as a route
27 	handler writes a response using `res.writeBody()` or similar means. If no
28 	route matches or if no route handler writes a response, the router will
29 	simply not handle the request and the HTTP server will automatically
30 	generate a 404 error.
31 
32 	Match_patterns:
33 		Match patterns are character sequences that can optionally contain
34 		placeholders or raw wildcards ("*"). Raw wild cards match any character
35 		sequence, while placeholders match only sequences containing no slash
36 		("/") characters.
37 
38 		Placeholders are started using a colon (":") and are directly followed
39 		by their name. The first "/" character (or the end of the match string)
40 		denotes the end of the placeholder name. The part of the string that
41 		matches a placeholder will be stored in the `HTTPServerRequest.params`
42 		map using the placeholder name as the key.
43 
44 		Match strings are subject to the following rules:
45 		$(UL
46 			$(LI A raw wildcard ("*") may only occur at the end of the match string)
47 			$(LI At least one character must be placed between any two placeholders or wildcards)
48 			$(LI The maximum allowed number of placeholders in a single match string is 64)
49 		)
50 
51 	Match_String_Examples:
52 		$(UL
53 			$(LI `"/foo/bar"` matches only `"/foo/bar"` itself)
54 			$(LI `"/foo/*"` matches `"/foo/"`, `"/foo/bar"`, `"/foo/bar/baz"` or _any other string beginning with `"/foo/"`)
55 			$(LI `"/:x/"` matches `"/foo/"`, `"/bar/"` and similar strings (and stores `"foo"`/`"bar"` in `req.params["x"]`), but not `"/foo/bar/"`)
56 			$(LI Matching partial path entries with wildcards is possible: `"/foo:x"` matches `"/foo"`, `"/foobar"`, but not `"/foo/bar"`)
57 			$(LI Multiple placeholders and raw wildcards can be combined: `"/:x/:y/*"`)
58 		)
59 */
60 final class URLRouter : HTTPServerRequestHandler {
61 	@safe:
62 
63 	private {
64 		MatchTree!Route m_routes;
65 		string m_prefix;
66 		bool m_computeBasePath;
67 	}
68 
69 	this(string prefix = null)
70 	{
71 		m_prefix = prefix;
72 	}
73 
74 	/** Sets a common prefix for all registered routes.
75 
76 		All routes will implicitly have this prefix prepended before being
77 		matched against incoming requests.
78 	*/
79 	@property string prefix() const { return m_prefix; }
80 
81 	/** Controls the computation of the "routerRootDir" parameter.
82 
83 		This parameter is available as `req.params["routerRootDir"]` and
84 		contains the relative path to the base path of the router. The base
85 		path is determined by the `prefix` property.
86 
87 		Note that this feature currently is requires dynamic memory allocations
88 		and is opt-in for this reason.
89 	*/
90 	@property void enableRootDir(bool enable) { m_computeBasePath = enable; }
91 
92 	/// Returns a single route handle to conveniently register multiple methods.
93 	URLRoute route(string path)
94 	in { assert(path.length, "Cannot register null or empty path!"); }
95 	do { return URLRoute(this, path); }
96 
97 	///
98 	unittest {
99 		void getFoo(scope HTTPServerRequest req, scope HTTPServerResponse res) { /* ... */ }
100 		void postFoo(scope HTTPServerRequest req, scope HTTPServerResponse res) { /* ... */ }
101 		void deleteFoo(scope HTTPServerRequest req, scope HTTPServerResponse res) { /* ... */ }
102 
103 		auto r = new URLRouter;
104 
105 		// using 'with' statement
106 		with (r.route("/foo")) {
107 			get(&getFoo);
108 			post(&postFoo);
109 			delete_(&deleteFoo);
110 		}
111 
112 		// using method chaining
113 		r.route("/foo")
114 			.get(&getFoo)
115 			.post(&postFoo)
116 			.delete_(&deleteFoo);
117 
118 		// without using route()
119 		r.get("/foo", &getFoo);
120 		r.post("/foo", &postFoo);
121 		r.delete_("/foo", &deleteFoo);
122 	}
123 
124 	/// Adds a new route for requests matching the specified HTTP method and pattern.
125 	URLRouter match(Handler)(HTTPMethod method, string path, Handler handler)
126 		if (isValidHandler!Handler)
127 	{
128 		import std.algorithm;
129 		assert(path.length, "Cannot register null or empty path!");
130 		assert(count(path, ':') <= maxRouteParameters, "Too many route parameters");
131 		logDebug("add route %s %s", method, path);
132 		m_routes.addTerminal(path, Route(method, path, handlerDelegate(handler)));
133 		return this;
134 	}
135 
136 	/// ditto
137 	URLRouter match(HTTPMethod method, string path, HTTPServerRequestDelegate handler)
138 	{
139 		return match!HTTPServerRequestDelegate(method, path, handler);
140 	}
141 
142 	/// Adds a new route for GET requests matching the specified pattern.
143 	URLRouter get(Handler)(string url_match, Handler handler) if (isValidHandler!Handler) { return match(HTTPMethod.GET, url_match, handler); }
144 	/// ditto
145 	URLRouter get(string url_match, HTTPServerRequestDelegate handler) { return match(HTTPMethod.GET, url_match, handler); }
146 
147 	/// Adds a new route for POST requests matching the specified pattern.
148 	URLRouter post(Handler)(string url_match, Handler handler) if (isValidHandler!Handler) { return match(HTTPMethod.POST, url_match, handler); }
149 	/// ditto
150 	URLRouter post(string url_match, HTTPServerRequestDelegate handler) { return match(HTTPMethod.POST, url_match, handler); }
151 
152 	/// Adds a new route for PUT requests matching the specified pattern.
153 	URLRouter put(Handler)(string url_match, Handler handler) if (isValidHandler!Handler) { return match(HTTPMethod.PUT, url_match, handler); }
154 	/// ditto
155 	URLRouter put(string url_match, HTTPServerRequestDelegate handler) { return match(HTTPMethod.PUT, url_match, handler); }
156 
157 	/// Adds a new route for DELETE requests matching the specified pattern.
158 	URLRouter delete_(Handler)(string url_match, Handler handler) if (isValidHandler!Handler) { return match(HTTPMethod.DELETE, url_match, handler); }
159 	/// ditto
160 	URLRouter delete_(string url_match, HTTPServerRequestDelegate handler) { return match(HTTPMethod.DELETE, url_match, handler); }
161 
162 	/// Adds a new route for PATCH requests matching the specified pattern.
163 	URLRouter patch(Handler)(string url_match, Handler handler) if (isValidHandler!Handler) { return match(HTTPMethod.PATCH, url_match, handler); }
164 	/// ditto
165 	URLRouter patch(string url_match, HTTPServerRequestDelegate handler) { return match(HTTPMethod.PATCH, url_match, handler); }
166 
167 	/// Adds a new route for requests matching the specified pattern, regardless of their HTTP verb.
168 	URLRouter any(Handler)(string url_match, Handler handler)
169 	{
170 		import std.traits;
171 		static HTTPMethod[] all_methods = [EnumMembers!HTTPMethod];
172 		foreach(immutable method; all_methods)
173 			match(method, url_match, handler);
174 
175 		return this;
176 	}
177 	/// ditto
178 	URLRouter any(string url_match, HTTPServerRequestDelegate handler) { return any!HTTPServerRequestDelegate(url_match, handler); }
179 
180 
181 	/** Rebuilds the internal matching structures to account for newly added routes.
182 
183 		This should be used after a lot of routes have been added to the router, to
184 		force eager computation of the match structures. The alternative is to
185 		let the router lazily compute the structures when the first request happens,
186 		which can delay this request.
187 	*/
188 	void rebuild()
189 	{
190 		m_routes.rebuildGraph();
191 	}
192 
193 	/// Handles a HTTP request by dispatching it to the registered route handlers.
194 	void handleRequest(HTTPServerRequest req, HTTPServerResponse res)
195 	{
196 		import vibe.core.path : PosixPath;
197 
198 		auto method = req.method;
199 
200 		string calcBasePath()
201 		@safe {
202 			import vibe.core.path : InetPath, relativeToWeb;
203 			auto p = InetPath(prefix.length ? prefix : "/");
204 			p.endsWithSlash = true;
205 			return p.relativeToWeb(req.requestPath).toString();
206 		}
207 
208 		string path;
209 
210 		// NOTE: Instead of failing, we just ignore requests with invalid path
211 		//       segments (i.e. containing path separators) here. Any request
212 		//       handlers later in the queue may still choose to process them
213 		//       appropriately.
214 		try path = (cast(PosixPath)req.requestPath).toString();
215 		catch (Exception e) return;
216 
217 		if (path.length < m_prefix.length || path[0 .. m_prefix.length] != m_prefix) return;
218 		path = path[m_prefix.length .. $];
219 
220 		while (true) {
221 			bool done = m_routes.match(path, (ridx, scope values) @safe {
222 				auto r = () @trusted { return &m_routes.getTerminalData(ridx); } ();
223 				if (r.method != method) return false;
224 
225 				logDebugV("route match: %s -> %s %s %s", req.requestPath, r.method, r.pattern, values);
226 				foreach (i, v; values) req.params[m_routes.getTerminalVarNames(ridx)[i]] = v;
227 				if (m_computeBasePath) req.params["routerRootDir"] = calcBasePath();
228 				r.cb(req, res);
229 				return res.headerWritten;
230 			});
231 			if (done) return;
232 
233 			if (method == HTTPMethod.HEAD) method = HTTPMethod.GET;
234 			else break;
235 		}
236 
237 		logDebug("no route match: %s %s", req.method, req.requestURL);
238 	}
239 
240 	/// Returns all registered routes as const AA
241 	const(Route)[] getAllRoutes()
242 	{
243 		auto routes = new Route[m_routes.terminalCount];
244 		foreach (i, ref r; routes)
245 			r = m_routes.getTerminalData(i);
246 		return routes;
247 	}
248 
249 	template isValidHandler(Handler) {
250 		@system {
251 			alias USDel = void delegate(HTTPServerRequest, HTTPServerResponse) @system;
252 			alias USFun = void function(HTTPServerRequest, HTTPServerResponse) @system;
253 			alias USDelS = void delegate(scope HTTPServerRequest, scope HTTPServerResponse) @system;
254 			alias USFunS = void function(scope HTTPServerRequest, scope HTTPServerResponse) @system;
255 		}
256 
257 		static if (
258 				is(Handler : HTTPServerRequestDelegate) ||
259 				is(Handler : HTTPServerRequestFunction) ||
260 				is(Handler : HTTPServerRequestHandler) ||
261 				is(Handler : HTTPServerRequestDelegateS) ||
262 				is(Handler : HTTPServerRequestFunctionS) ||
263 				is(Handler : HTTPServerRequestHandlerS)
264 			)
265 		{
266 			enum isValidHandler = true;
267 		} else static if (
268 				is(Handler : USDel) || is(Handler : USFun) ||
269 				is(Handler : USDelS) || is(Handler : USFunS)
270 			)
271 		{
272 			enum isValidHandler = true;
273 		} else {
274 			enum isValidHandler = false;
275 		}
276 	}
277 
278 	static void delegate(HTTPServerRequest, HTTPServerResponse) @safe handlerDelegate(Handler)(Handler handler)
279 	{
280 		import std.traits : isFunctionPointer;
281 		static if (isFunctionPointer!Handler) return handlerDelegate(() @trusted { return toDelegate(handler); } ());
282 		else static if (is(Handler == class) || is(Handler == interface)) return &handler.handleRequest;
283 		else static if (__traits(compiles, () @safe { handler(HTTPServerRequest.init, HTTPServerResponse.init); } ())) return handler;
284 		else return (req, res) @trusted { handler(req, res); };
285 	}
286 
287 	unittest {
288 		static assert(isValidHandler!HTTPServerRequestFunction);
289 		static assert(isValidHandler!HTTPServerRequestDelegate);
290 		static assert(isValidHandler!HTTPServerRequestHandler);
291 		static assert(isValidHandler!HTTPServerRequestFunctionS);
292 		static assert(isValidHandler!HTTPServerRequestDelegateS);
293 		static assert(isValidHandler!HTTPServerRequestHandlerS);
294 		static assert(isValidHandler!(void delegate(HTTPServerRequest req, HTTPServerResponse res) @system));
295 		static assert(isValidHandler!(void function(HTTPServerRequest req, HTTPServerResponse res) @system));
296 		static assert(isValidHandler!(void delegate(scope HTTPServerRequest req, scope HTTPServerResponse res) @system));
297 		static assert(isValidHandler!(void function(scope HTTPServerRequest req, scope HTTPServerResponse res) @system));
298 		static assert(!isValidHandler!(int delegate(HTTPServerRequest req, HTTPServerResponse res) @system));
299 		static assert(!isValidHandler!(int delegate(HTTPServerRequest req, HTTPServerResponse res) @safe));
300 		void test(H)(H h)
301 		{
302 			static assert(isValidHandler!H);
303 		}
304 		test((HTTPServerRequest req, HTTPServerResponse res) {});
305 	}
306 }
307 
308 ///
309 @safe unittest {
310 	import vibe.http.fileserver;
311 
312 	void addGroup(HTTPServerRequest req, HTTPServerResponse res)
313 	{
314 		// Route variables are accessible via the params map
315 		logInfo("Getting group %s for user %s.", req.params["groupname"], req.params["username"]);
316 	}
317 
318 	void deleteUser(HTTPServerRequest req, HTTPServerResponse res)
319 	{
320 		// ...
321 	}
322 
323 	void auth(HTTPServerRequest req, HTTPServerResponse res)
324 	{
325 		// TODO: check req.session to see if a user is logged in and
326 		//       write an error page or throw an exception instead.
327 	}
328 
329 	void setup()
330 	{
331 		auto router = new URLRouter;
332 		// Matches all GET requests for /users/*/groups/* and places
333 		// the place holders in req.params as 'username' and 'groupname'.
334 		router.get("/users/:username/groups/:groupname", &addGroup);
335 
336 		// Matches all requests. This can be useful for authorization and
337 		// similar tasks. The auth method will only write a response if the
338 		// user is _not_ authorized. Otherwise, the router will fall through
339 		// and continue with the following routes.
340 		router.any("*", &auth);
341 
342 		// Matches a POST request
343 		router.post("/users/:username/delete", &deleteUser);
344 
345 		// Matches all GET requests in /static/ such as /static/img.png or
346 		// /static/styles/sty.css
347 		router.get("/static/*", serveStaticFiles("public/"));
348 
349 		// Setup a HTTP server...
350 		auto settings = new HTTPServerSettings;
351 		// ...
352 
353 		// The router can be directly passed to the listenHTTP function as
354 		// the main request handler.
355 		listenHTTP(settings, router);
356 	}
357 }
358 
359 /** Using nested routers to map components to different sub paths. A component
360 	could for example be an embedded blog engine.
361 */
362 @safe unittest {
363 	// some embedded component:
364 
365 	void showComponentHome(HTTPServerRequest req, HTTPServerResponse res)
366 	{
367 		// ...
368 	}
369 
370 	void showComponentUser(HTTPServerRequest req, HTTPServerResponse res)
371 	{
372 		// ...
373 	}
374 
375 	void registerComponent(URLRouter router)
376 	{
377 		router.get("/", &showComponentHome);
378 		router.get("/users/:user", &showComponentUser);
379 	}
380 
381 	// main application:
382 
383 	void showHome(HTTPServerRequest req, HTTPServerResponse res)
384 	{
385 		// ...
386 	}
387 
388 	void setup()
389 	{
390 		auto c1router = new URLRouter("/component1");
391 		registerComponent(c1router);
392 
393 		auto mainrouter = new URLRouter;
394 		mainrouter.get("/", &showHome);
395 		// forward all unprocessed requests to the component router
396 		mainrouter.any("*", c1router);
397 
398 		// now the following routes will be matched:
399 		// / -> showHome
400 		// /component1/ -> showComponentHome
401 		// /component1/users/:user -> showComponentUser
402 
403 		// Start the HTTP server
404 		auto settings = new HTTPServerSettings;
405 		// ...
406 		listenHTTP(settings, mainrouter);
407 	}
408 }
409 
410 @safe unittest { // issue #1668
411 	auto r = new URLRouter;
412 	r.get("/", (req, res) {
413 		if ("foo" in req.headers)
414 			res.writeBody("bar");
415 	});
416 
417 	r.get("/", (scope req, scope res) {
418 		if ("foo" in req.headers)
419 			res.writeBody("bar");
420 	});
421 	r.get("/", (req, res) {});
422 	r.post("/", (req, res) {});
423 	r.put("/", (req, res) {});
424 	r.delete_("/", (req, res) {});
425 	r.patch("/", (req, res) {});
426 	r.any("/", (req, res) {});
427 }
428 
429 @safe unittest { // issue #1866
430 	auto r = new URLRouter;
431 	r.match(HTTPMethod.HEAD, "/foo", (scope req, scope res) { res.writeVoidBody; });
432 	r.match(HTTPMethod.HEAD, "/foo", (req, res) { res.writeVoidBody; });
433 	r.match(HTTPMethod.HEAD, "/foo", (scope HTTPServerRequest req, scope HTTPServerResponse res) { res.writeVoidBody; });
434 	r.match(HTTPMethod.HEAD, "/foo", (HTTPServerRequest req, HTTPServerResponse res) { res.writeVoidBody; });
435 
436 	auto r2 = new URLRouter;
437 	r.match(HTTPMethod.HEAD, "/foo", r2);
438 }
439 
440 @safe unittest {
441 	import vibe.inet.url;
442 
443 	auto router = new URLRouter;
444 	string result;
445 	void a(HTTPServerRequest req, HTTPServerResponse) { result ~= "A"; }
446 	void b(HTTPServerRequest req, HTTPServerResponse) { result ~= "B"; }
447 	void c(HTTPServerRequest req, HTTPServerResponse) { assert(req.params["test"] == "x", "Wrong variable contents: "~req.params["test"]); result ~= "C"; }
448 	void d(HTTPServerRequest req, HTTPServerResponse) { assert(req.params["test"] == "y", "Wrong variable contents: "~req.params["test"]); result ~= "D"; }
449 	router.get("/test", &a);
450 	router.post("/test", &b);
451 	router.get("/a/:test", &c);
452 	router.get("/a/:test/", &d);
453 
454 	auto res = createTestHTTPServerResponse();
455 	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/")), res);
456 	assert(result == "", "Matched for non-existent / path");
457 	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test")), res);
458 	assert(result == "A", "Didn't match a simple GET request");
459 	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test"), HTTPMethod.POST), res);
460 	assert(result == "AB", "Didn't match a simple POST request");
461 	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/a/"), HTTPMethod.GET), res);
462 	assert(result == "AB", "Matched empty variable. "~result);
463 	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/a/x"), HTTPMethod.GET), res);
464 	assert(result == "ABC", "Didn't match a trailing 1-character var.");
465 	// currently fails due to Path not accepting "//"
466 	//router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/a//"), HTTPMethod.GET), res);
467 	//assert(result == "ABC", "Matched empty string or slash as var. "~result);
468 	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/a/y/"), HTTPMethod.GET), res);
469 	assert(result == "ABCD", "Didn't match 1-character infix variable.");
470 }
471 
472 @safe unittest {
473 	import vibe.inet.url;
474 
475 	auto router = new URLRouter("/test");
476 
477 	string result;
478 	void a(HTTPServerRequest req, HTTPServerResponse) { result ~= "A"; }
479 	void b(HTTPServerRequest req, HTTPServerResponse) { result ~= "B"; }
480 	router.get("/x", &a);
481 	router.get("/y", &b);
482 
483 	auto res = createTestHTTPServerResponse();
484 	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test")), res);
485 	assert(result == "");
486 	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test/x")), res);
487 	assert(result == "A");
488 	router.handleRequest(createTestHTTPServerRequest(URL("http://localhost/test/y")), res);
489 	assert(result == "AB");
490 }
491 
492 @safe unittest {
493 	string ensureMatch(string pattern, string local_uri, string[string] expected_params = null)
494 	{
495 		import vibe.inet.url : URL;
496 		string ret = local_uri ~ " did not match " ~ pattern;
497 		auto r = new URLRouter;
498 		r.get(pattern, (req, res) {
499 			ret = null;
500 			foreach (k, v; expected_params) {
501 				if (k !in req.params) { ret = "Parameter "~k~" was not matched."; return; }
502 				if (req.params[k] != v) { ret = "Parameter "~k~" is '"~req.params[k]~"' instead of '"~v~"'."; return; }
503 			}
504 		});
505 		auto req = createTestHTTPServerRequest(URL("http://localhost"~local_uri));
506 		auto res = createTestHTTPServerResponse();
507 		r.handleRequest(req, res);
508 		return ret;
509 	}
510 
511 	assert(ensureMatch("/foo bar/", "/foo%20bar/") is null);   // normalized pattern: "/foo%20bar/"
512 	//assert(ensureMatch("/foo%20bar/", "/foo%20bar/") is null); // normalized pattern: "/foo%20bar/"
513 	assert(ensureMatch("/foo/bar/", "/foo/bar/") is null);     // normalized pattern: "/foo/bar/"
514 	//assert(ensureMatch("/foo/bar/", "/foo%2fbar/") !is null);
515 	//assert(ensureMatch("/foo%2fbar/", "/foo%2fbar/") is null); // normalized pattern: "/foo%2Fbar/"
516 	//assert(ensureMatch("/foo%2Fbar/", "/foo%2fbar/") is null); // normalized pattern: "/foo%2Fbar/"
517 	//assert(ensureMatch("/foo%2fbar/", "/foo%2Fbar/") is null);
518 	//assert(ensureMatch("/foo%2fbar/", "/foo/bar/") !is null);
519 	//assert(ensureMatch("/:foo/", "/foo%2Fbar/", ["foo": "foo/bar"]) is null);
520 	assert(ensureMatch("/:foo/", "/foo/bar/") !is null);
521 }
522 
523 unittest { // issue #2561
524 	import vibe.http.server : createTestHTTPServerRequest, createTestHTTPServerResponse;
525 	import vibe.inet.url : URL;
526 	import vibe.stream.memory : createMemoryOutputStream;
527 
528 	Route[] routes = [
529 		Route(HTTPMethod.PUT, "/public/devices/commandList"),
530 		Route(HTTPMethod.PUT, "/public/devices/logicStateList"),
531 		Route(HTTPMethod.OPTIONS, "/public/devices/commandList"),
532 		Route(HTTPMethod.OPTIONS, "/public/devices/logicStateList"),
533 		Route(HTTPMethod.PUT, "/public/mnemoschema"),
534 		Route(HTTPMethod.PUT, "/public/static"),
535 		Route(HTTPMethod.PUT, "/public/dynamic"),
536 		Route(HTTPMethod.PUT, "/public/info"),
537 		Route(HTTPMethod.PUT, "/public/info-network"),
538 		Route(HTTPMethod.PUT, "/public/events"),
539 		Route(HTTPMethod.PUT, "/public/eventList"),
540 		Route(HTTPMethod.PUT, "/public/availBatteryModels"),
541 		Route(HTTPMethod.OPTIONS, "/public/availBatteryModels"),
542 		Route(HTTPMethod.OPTIONS, "/public/dynamic"),
543 		Route(HTTPMethod.OPTIONS, "/public/eventList"),
544 		Route(HTTPMethod.OPTIONS, "/public/events"),
545 		Route(HTTPMethod.OPTIONS, "/public/info"),
546 		Route(HTTPMethod.OPTIONS, "/public/info-network"),
547 		Route(HTTPMethod.OPTIONS, "/public/mnemoschema"),
548 		Route(HTTPMethod.OPTIONS, "/public/static"),
549 		Route(HTTPMethod.PUT, "/settings/admin/getinfo"),
550 		Route(HTTPMethod.PUT, "/settings/admin/setconf"),
551 		Route(HTTPMethod.PUT, "/settings/admin/checksetaccess"),
552 		Route(HTTPMethod.OPTIONS, "/settings/admin/checksetaccess"),
553 		Route(HTTPMethod.OPTIONS, "/settings/admin/getinfo"),
554 		Route(HTTPMethod.OPTIONS, "/settings/admin/setconf"),
555 	];
556 
557 	auto router = new URLRouter;
558 
559 	foreach (r; routes)
560 		router.match(r.method, r.pattern, (req, res) {
561 			res.writeBody("OK");
562 		});
563 
564 	{ // make sure unmatched routes are not handled by the router
565 		auto req = createTestHTTPServerRequest(URL("http://localhost/foobar"), HTTPMethod.PUT);
566 		auto res = createTestHTTPServerResponse();
567 		router.handleRequest(req, res);
568 		assert(!res.headerWritten);
569 	}
570 
571 	// ensure all routes are matched
572 	foreach (r; routes) {
573 		auto url = URL("http://localhost"~r.pattern);
574 		auto output = createMemoryOutputStream();
575 		auto req = createTestHTTPServerRequest(url, r.method);
576 		auto res = createTestHTTPServerResponse(output, null, TestHTTPResponseMode.bodyOnly);
577 		router.handleRequest(req, res);
578 		//assert(res.headerWritten);
579 		assert(output.data == "OK");
580 	}
581 }
582 
583 
584 /**
585 	Convenience abstraction for a single `URLRouter` route.
586 
587 	See `URLRouter.route` for a usage example.
588 */
589 struct URLRoute {
590 @safe:
591 
592 	URLRouter router;
593 	string path;
594 
595 	ref URLRoute get(Handler)(Handler h) { router.get(path, h); return this; }
596 	ref URLRoute post(Handler)(Handler h) { router.post(path, h); return this; }
597 	ref URLRoute put(Handler)(Handler h) { router.put(path, h); return this; }
598 	ref URLRoute delete_(Handler)(Handler h) { router.delete_(path, h); return this; }
599 	ref URLRoute patch(Handler)(Handler h) { router.patch(path, h); return this; }
600 	ref URLRoute any(Handler)(Handler h) { router.any(path, h); return this; }
601 	ref URLRoute match(Handler)(HTTPMethod method, Handler h) { router.match(method, path, h); return this; }
602 }
603 
604 
605 private enum maxRouteParameters = 64;
606 
607 private struct Route {
608 	HTTPMethod method;
609 	string pattern;
610 	HTTPServerRequestDelegate cb;
611 }
612 
613 private string skipPathNode(string str, ref size_t idx)
614 @safe {
615 	size_t start = idx;
616 	while( idx < str.length && str[idx] != '/' ) idx++;
617 	return str[start .. idx];
618 }
619 
620 private string skipPathNode(ref string str)
621 @safe {
622 	size_t idx = 0;
623 	auto ret = skipPathNode(str, idx);
624 	str = str[idx .. $];
625 	return ret;
626 }
627 
628 private struct MatchTree(T) {
629 @safe:
630 
631 	import std.algorithm : countUntil;
632 	import std.array : array;
633 
634 	private {
635 		alias NodeIndex = uint;
636 		alias TerminalTagIndex = uint;
637 		alias TerminalIndex = ushort;
638 		alias VarIndex = ushort;
639 
640 		struct Node {
641 			TerminalTagIndex terminalsStart; // slice into m_terminalTags
642 			TerminalTagIndex terminalsEnd;
643 			NodeIndex[ubyte.max+1] edges = NodeIndex.max; // character -> index into m_nodes
644 		}
645 		struct TerminalTag {
646 			TerminalIndex index; // index into m_terminals array
647 			VarIndex var = VarIndex.max; // index into Terminal.varNames/varValues or VarIndex.max
648 		}
649 		struct Terminal {
650 			string pattern;
651 			T data;
652 			string[] varNames;
653 			VarIndex[NodeIndex] varMap;
654 		}
655 		Node[] m_nodes; // all nodes as a single array
656 		TerminalTag[] m_terminalTags;
657 		Terminal[] m_terminals;
658 
659 		enum TerminalChar = 0;
660 		bool m_upToDate = false;
661 	}
662 
663 	@property size_t terminalCount() const { return m_terminals.length; }
664 
665 	void addTerminal(string pattern, T data)
666 	{
667 		assert(m_terminals.length < TerminalIndex.max, "Attempt to register too many routes.");
668 		m_terminals ~= Terminal(pattern, data, null, null);
669 		m_upToDate = false;
670 	}
671 
672 	bool match(string text, scope bool delegate(size_t terminal, scope string[] vars) @safe del)
673 	{
674 		// lazily update the match graph
675 		if (!m_upToDate) rebuildGraph();
676 
677 		return doMatch(text, del);
678 	}
679 
680 	const(string)[] getTerminalVarNames(size_t terminal) const { return m_terminals[terminal].varNames; }
681 	ref inout(T) getTerminalData(size_t terminal) inout { return m_terminals[terminal].data; }
682 
683 	void print()
684 	const {
685 		import std.algorithm : map;
686 		import std.array : join;
687 		import std.conv : to;
688 		import std.range : iota;
689 		import std.string : format;
690 
691 		logInfo("Nodes:");
692 		foreach (i, n; m_nodes) {
693 			logInfo("  %s %s", i, m_terminalTags[n.terminalsStart .. n.terminalsEnd]
694 				.map!(t => format("T%s%s", t.index, t.var != VarIndex.max ? "("~m_terminals[t.index].varNames[t.var]~")" : "")).join(" "));
695 			//logInfo("  %s %s-%s", i, n.terminalsStart, n.terminalsEnd);
696 
697 
698 			static string mapChar(ubyte ch) {
699 				if (ch == TerminalChar) return "$";
700 				if (ch >= '0' && ch <= '9') return to!string(cast(dchar)ch);
701 				if (ch >= 'a' && ch <= 'z') return to!string(cast(dchar)ch);
702 				if (ch >= 'A' && ch <= 'Z') return to!string(cast(dchar)ch);
703 				if (ch == '/') return "/";
704 				if (ch == '^') return "^";
705 				return ch.to!string;
706 			}
707 
708 			void printRange(uint node, ubyte from, ubyte to)
709 			{
710 				if (to - from <= 10) logInfo("    %s -> %s", iota(from, cast(uint)to+1).map!(ch => mapChar(cast(ubyte)ch)).join("|"), node);
711 				else logInfo("    %s-%s -> %s", mapChar(from), mapChar(to), node);
712 			}
713 
714 			auto last_to = NodeIndex.max;
715 			ubyte last_ch = 0;
716 			foreach (ch, e; n.edges)
717 				if (e != last_to) {
718 					if (last_to != NodeIndex.max)
719 						printRange(last_to, last_ch, cast(ubyte)(ch-1));
720 					last_ch = cast(ubyte)ch;
721 					last_to = e;
722 				}
723 			if (last_to != NodeIndex.max)
724 				printRange(last_to, last_ch, ubyte.max);
725 		}
726 	}
727 
728 	private bool doMatch(string text, scope bool delegate(size_t terminal, scope string[] vars) @safe del)
729 	const {
730 		string[maxRouteParameters] vars_buf;// = void;
731 
732 		import std.algorithm : canFind;
733 
734 		// first, determine the end node, if any
735 		auto n = matchTerminals(text);
736 		if (!n) return false;
737 
738 		// then, go through the terminals and match their variables
739 		foreach (ref t; m_terminalTags[n.terminalsStart .. n.terminalsEnd]) {
740 			auto term = &m_terminals[t.index];
741 			auto vars = vars_buf[0 .. term.varNames.length];
742 			matchVars(vars, term, text);
743 			if (vars.canFind!(v => v.length == 0)) continue; // all variables must be non-empty to match
744 			if (del(t.index, vars)) return true;
745 		}
746 		return false;
747 	}
748 
749 	private inout(Node)* matchTerminals(string text)
750 	inout {
751 		if (!m_nodes.length) return null;
752 
753 		auto n = &m_nodes[0];
754 
755 		// follow the path through the match graph
756 		foreach (i, char ch; text) {
757 			auto nidx = n.edges[cast(size_t)ch];
758 			if (nidx == NodeIndex.max) return null;
759 			n = &m_nodes[nidx];
760 		}
761 
762 		// finally, find a matching terminal node
763 		auto nidx = n.edges[TerminalChar];
764 		if (nidx == NodeIndex.max) return null;
765 		n = &m_nodes[nidx];
766 		return n;
767 	}
768 
769 	private void matchVars(string[] dst, in Terminal* term, string text)
770 	const {
771 		NodeIndex nidx = 0;
772 		VarIndex activevar = VarIndex.max;
773 		size_t activevarstart;
774 
775 		dst[] = null;
776 
777 		// folow the path throgh the match graph
778 		foreach (i, char ch; text) {
779 			auto var = term.varMap.get(nidx, VarIndex.max);
780 
781 			// detect end of variable
782 			if (var != activevar && activevar != VarIndex.max) {
783 				dst[activevar] = text[activevarstart .. i-1];
784 				activevar = VarIndex.max;
785 			}
786 
787 			// detect beginning of variable
788 			if (var != VarIndex.max && activevar == VarIndex.max) {
789 				activevar = var;
790 				activevarstart = i;
791 			}
792 
793 			nidx = m_nodes[nidx].edges[cast(ubyte)ch];
794 			assert(nidx != NodeIndex.max);
795 		}
796 
797 		// terminate any active varible with the end of the input string or with the last character
798 		auto var = term.varMap.get(nidx, VarIndex.max);
799 		if (activevar != VarIndex.max) dst[activevar] = text[activevarstart .. (var == activevar ? $ : $-1)];
800 	}
801 
802 	private void rebuildGraph()
803 	@trusted {
804 		import std.array : appender;
805 		import std.conv : to;
806 
807 		if (m_upToDate) return;
808 		m_upToDate = true;
809 
810 		m_nodes = null;
811 		m_terminalTags = null;
812 
813 		if (!m_terminals.length) return;
814 
815 		MatchGraphBuilder builder;
816 		foreach (i, ref t; m_terminals) {
817 			t.varNames = builder.insert(t.pattern, i.to!TerminalIndex);
818 			assert(t.varNames.length <= VarIndex.max, "Too many variables in route.");
819 		}
820 		//builder.print();
821 		builder.disambiguate();
822 
823 		auto nodemap = new NodeIndex[builder.m_nodes.length];
824 		nodemap[] = NodeIndex.max;
825 
826 		auto nodes = appender!(Node[]);
827 		nodes.reserve(1024);
828 		auto termtags = appender!(TerminalTag[]);
829 		termtags.reserve(1024);
830 
831 		NodeIndex process(NodeIndex n)
832 		{
833 			import std.algorithm : canFind;
834 
835 			if (nodemap[n] != NodeIndex.max) return nodemap[n];
836 			auto nmidx = cast(NodeIndex)nodes.data.length;
837 			nodemap[n] = nmidx;
838 			nodes.put(Node.init);
839 
840 			Node nn;
841 			nn.terminalsStart = termtags.data.length.to!TerminalTagIndex;
842 			foreach (t; builder.m_nodes[n].terminals) {
843 				auto var = cast(VarIndex)t.var;
844 				assert(!termtags.data[nn.terminalsStart .. $].canFind!(u => u.index == t.index && u.var == var));
845 				termtags ~= TerminalTag(cast(TerminalIndex)t.index, var);
846 				if (var != VarIndex.max)
847 					m_terminals[t.index].varMap[nmidx] = var;
848 			}
849 			nn.terminalsEnd = termtags.data.length.to!TerminalTagIndex;
850 			foreach (ch, targets; builder.m_nodes[n].edges)
851 				foreach (to; builder.m_edgeEntries.getItems(targets))
852 					nn.edges[ch] = process(to);
853 
854 			nodes.data[nmidx] = nn;
855 
856 			return nmidx;
857 		}
858 		assert(builder.m_edgeEntries.hasLength(builder.m_nodes[0].edges['^'], 1),
859 			"Graph must be disambiguated before purging.");
860 		process(builder.m_edgeEntries.getItems(builder.m_nodes[0].edges['^']).front);
861 
862 		m_nodes = nodes.data;
863 		m_terminalTags = termtags.data;
864 
865 		logDebug("Match tree has %s (of %s in the builder) nodes, %s terminals", m_nodes.length, builder.m_nodes.length, m_terminals.length);
866 	}
867 }
868 
869 unittest {
870 	import std.string : format;
871 	MatchTree!int m;
872 
873 	void testMatch(string str, size_t[] terms, string[] vars)
874 	{
875 		size_t[] mterms;
876 		string[] mvars;
877 		m.match(str, (t, scope vals) {
878 			mterms ~= t;
879 			mvars ~= vals;
880 			return false;
881 		});
882 		assert(mterms == terms, format("Mismatched terminals: %s (expected %s)", mterms, terms));
883 		assert(mvars == vars, format("Mismatched variables; %s (expected %s)", mvars, vars));
884 	}
885 
886 	m.addTerminal("a", 0);
887 	m.addTerminal("b", 0);
888 	m.addTerminal("ab", 0);
889 	m.rebuildGraph();
890 	assert(m.getTerminalVarNames(0) == []);
891 	assert(m.getTerminalVarNames(1) == []);
892 	assert(m.getTerminalVarNames(2) == []);
893 	testMatch("a", [0], []);
894 	testMatch("ab", [2], []);
895 	testMatch("abc", [], []);
896 	testMatch("b", [1], []);
897 
898 	m = MatchTree!int.init;
899 	m.addTerminal("ab", 0);
900 	m.addTerminal("a*", 0);
901 	m.rebuildGraph();
902 	assert(m.getTerminalVarNames(0) == []);
903 	assert(m.getTerminalVarNames(1) == []);
904 	testMatch("a", [1], []);
905 	testMatch("ab", [0, 1], []);
906 	testMatch("abc", [1], []);
907 
908 	m = MatchTree!int.init;
909 	m.addTerminal("ab", 0);
910 	m.addTerminal("a:var", 0);
911 	m.rebuildGraph();
912 	assert(m.getTerminalVarNames(0) == []);
913 	assert(m.getTerminalVarNames(1) == ["var"], format("%s", m.getTerminalVarNames(1)));
914 	testMatch("a", [], []); // vars may not be empty
915 	testMatch("ab", [0, 1], ["b"]);
916 	testMatch("abc", [1], ["bc"]);
917 
918 	m = MatchTree!int.init;
919 	m.addTerminal(":var1/:var2", 0);
920 	m.addTerminal("a/:var3", 0);
921 	m.addTerminal(":var4/b", 0);
922 	m.rebuildGraph();
923 	assert(m.getTerminalVarNames(0) == ["var1", "var2"]);
924 	assert(m.getTerminalVarNames(1) == ["var3"]);
925 	assert(m.getTerminalVarNames(2) == ["var4"]);
926 	testMatch("a", [], []);
927 	testMatch("a/a", [0, 1], ["a", "a", "a"]);
928 	testMatch("a/b", [0, 1, 2], ["a", "b", "b", "a"]);
929 	testMatch("a/bc", [0, 1], ["a", "bc", "bc"]);
930 	testMatch("ab/b", [0, 2], ["ab", "b", "ab"]);
931 	testMatch("ab/bc", [0], ["ab", "bc"]);
932 
933 	m = MatchTree!int.init;
934 	m.addTerminal(":var1/", 0);
935 	m.rebuildGraph();
936 	assert(m.getTerminalVarNames(0) == ["var1"]);
937 	testMatch("ab/", [0], ["ab"]);
938 	testMatch("ab", [], []);
939 	testMatch("/ab", [], []);
940 	testMatch("a/b", [], []);
941 	testMatch("ab//", [], []);
942 }
943 
944 
945 private struct MatchGraphBuilder {
946 @safe:
947 	import std.container.array : Array;
948 	import std.array : array;
949 	import std.algorithm : filter;
950 	import std.string : format;
951 
952 	alias NodeIndex = uint;
953 	alias TerminalIndex = ushort;
954 	alias VarIndex = ushort;
955 	alias NodeSet = LinkedSetBacking!NodeIndex.Handle;
956 
957 	private {
958 		enum TerminalChar = 0;
959 		struct TerminalTag {
960 			TerminalIndex index;
961 			VarIndex var = VarIndex.max;
962 		}
963 		struct Node {
964 			Array!TerminalTag terminals;
965 			NodeSet[ubyte.max+1] edges;
966 		}
967 		Array!Node m_nodes;
968 		LinkedSetBacking!NodeIndex m_edgeEntries;
969 	}
970 
971 	@disable this(this);
972 
973 	string[] insert(string pattern, TerminalIndex terminal)
974 	{
975 		import std.algorithm : canFind;
976 
977 		auto full_pattern = pattern;
978 		string[] vars;
979 		if (!m_nodes.length) addNode();
980 
981 		// create start node and connect to zero node
982 		auto nidx = addNode();
983 		addEdge(0, nidx, '^', terminal);
984 
985 		while (pattern.length) {
986 			auto ch = pattern[0];
987 			if (ch == '*') {
988 				assert(pattern.length == 1, "Asterisk is only allowed at the end of a pattern: "~full_pattern);
989 				pattern = null;
990 
991 				foreach (v; ubyte.min .. ubyte.max+1) {
992 					if (v == TerminalChar) continue;
993 					addEdge(nidx, nidx, cast(ubyte)v, terminal);
994 				}
995 			} else if (ch == ':') {
996 				pattern = pattern[1 .. $];
997 				auto name = skipPathNode(pattern);
998 				assert(name.length > 0, "Missing placeholder name: "~full_pattern);
999 				assert(!vars.canFind(name), "Duplicate placeholder name ':"~name~"': '"~full_pattern~"'");
1000 				auto varidx = cast(VarIndex)vars.length;
1001 				vars ~= name;
1002 				assert(!pattern.length || (pattern[0] != '*' && pattern[0] != ':'),
1003 					"Cannot have two placeholders directly follow each other.");
1004 
1005 				foreach (v; ubyte.min .. ubyte.max+1) {
1006 					if (v == TerminalChar || v == '/') continue;
1007 					addEdge(nidx, nidx, cast(ubyte)v, terminal, varidx);
1008 				}
1009 			} else {
1010 				nidx = addEdge(nidx, ch, terminal);
1011 				pattern = pattern[1 .. $];
1012 			}
1013 		}
1014 
1015 		addEdge(nidx, TerminalChar, terminal);
1016 		return vars;
1017 	}
1018 
1019 	void disambiguate()
1020 	@trusted {
1021 		import std.algorithm : map, sum;
1022 		import std.array : appender, join;
1023 
1024 		//logInfo("Disambiguate with %s initial nodes", m_nodes.length);
1025 		if (!m_nodes.length) return;
1026 
1027 		import vibe.utils.hashmap;
1028 		HashMap!(LinkedSetHash, NodeIndex) combined_nodes;
1029 		Array!bool visited;
1030 		visited.length = m_nodes.length * 2;
1031 		Stack!NodeIndex node_stack;
1032 		node_stack.reserve(m_nodes.length);
1033 		node_stack.push(0);
1034 		while (!node_stack.empty) {
1035 			auto n = node_stack.pop();
1036 
1037 			while (n >= visited.length) visited.length = visited.length * 2;
1038 			if (visited[n]) continue;
1039 			//logInfo("Disambiguate %s (stack=%s)", n, node_stack.fill);
1040 			visited[n] = true;
1041 
1042 			foreach (ch; ubyte.min .. ubyte.max+1) {
1043 				auto chnodes = m_nodes[n].edges[ch];
1044 				LinkedSetHash chhash = m_edgeEntries.getHash(chnodes);
1045 
1046 				// handle trivial cases
1047 				if (m_edgeEntries.hasMaxLength(chnodes, 1))
1048 					continue;
1049 
1050 				// generate combined state for ambiguous edges
1051 				if (auto pn = () @trusted { return chhash in combined_nodes; } ()) {
1052 					m_nodes[n].edges[ch] = m_edgeEntries.create(*pn);
1053 					assert(m_edgeEntries.hasLength(m_nodes[n].edges[ch], 1));
1054 					continue;
1055 				}
1056 
1057 				// for new combinations, create a new node
1058 				NodeIndex ncomb = addNode();
1059 				combined_nodes[chhash] = ncomb;
1060 
1061 				// write all edges
1062 				size_t idx = 0;
1063 				foreach (to_ch; ubyte.min .. ubyte.max+1) {
1064 					auto e = &m_nodes[ncomb].edges[to_ch];
1065 					foreach (chn; m_edgeEntries.getItems(chnodes))
1066 						m_edgeEntries.insert(e, m_edgeEntries.getItems(m_nodes[chn].edges[to_ch]));
1067 				}
1068 
1069 				// add terminal indices
1070 				foreach (chn; m_edgeEntries.getItems(chnodes))
1071 					foreach (t; m_nodes[chn].terminals)
1072 						addTerminal(ncomb, t.index, t.var);
1073 				foreach (i; 1 .. m_nodes[ncomb].terminals.length)
1074 					assert(m_nodes[ncomb].terminals[0] != m_nodes[ncomb].terminals[i]);
1075 
1076 				m_nodes[n].edges[ch] = m_edgeEntries.create(ncomb);
1077 				assert(m_edgeEntries.hasLength(m_nodes[n].edges[ch], 1));
1078 			}
1079 
1080 			// process nodes recursively
1081 			foreach (ch; ubyte.min .. ubyte.max+1) {
1082 				// should only have single out-edges now
1083 				assert(m_edgeEntries.hasMaxLength(m_nodes[n].edges[ch], 1));
1084 				foreach (e; m_edgeEntries.getItems(m_nodes[n].edges[ch]))
1085 					node_stack.push(e);
1086 			}
1087 		}
1088 
1089 		import std.algorithm.sorting : sort;
1090 		foreach (ref n; m_nodes)
1091 			n.terminals[].sort!((a, b) => a.index < b.index)();
1092 
1093 		debug logDebug("Disambiguate done: %s nodes, %s max stack size", m_nodes.length, node_stack.maxSize);
1094 	}
1095 
1096 	void print()
1097 	const @trusted {
1098 		import std.algorithm : map;
1099 		import std.array : join;
1100 		import std.conv : to;
1101 		import std.string : format;
1102 
1103 		logInfo("Nodes:");
1104 		size_t i = 0;
1105 		foreach (n; m_nodes) {
1106 			string mapChar(ubyte ch) {
1107 				if (ch == TerminalChar) return "$";
1108 				if (ch >= '0' && ch <= '9') return to!string(cast(dchar)ch);
1109 				if (ch >= 'a' && ch <= 'z') return to!string(cast(dchar)ch);
1110 				if (ch >= 'A' && ch <= 'Z') return to!string(cast(dchar)ch);
1111 				if (ch == '^') return "^";
1112 				if (ch == '/') return "/";
1113 				return format("$%s", ch);
1114 			}
1115 			logInfo("  %s: %s", i, n.terminals[].map!(t => t.var != VarIndex.max ? format("T%s(%s)", t.index, t.var) : format("T%s", t.index)).join(" "));
1116 			ubyte first_char;
1117 			LinkedSetHash list_hash;
1118 			NodeSet list;
1119 
1120 			void printEdges(ubyte last_char) {
1121 				if (!list.empty) {
1122 					string targets;
1123 					foreach (tn; m_edgeEntries.getItems(list))
1124 						targets ~= format(" %s", tn);
1125 					if (targets.length > 0)
1126 						logInfo("    [%s ... %s] -> %s", mapChar(first_char), mapChar(last_char), targets);
1127 				}
1128 			}
1129 			foreach (ch, tnodes; n.edges) {
1130 				auto h = m_edgeEntries.getHash(tnodes);
1131 				if (h != list_hash) {
1132 					printEdges(cast(ubyte)(ch-1));
1133 					list_hash = h;
1134 					list = tnodes;
1135 					first_char = cast(ubyte)ch;
1136 				}
1137 			}
1138 			printEdges(ubyte.max);
1139 			i++;
1140 		}
1141 	}
1142 
1143 	private void addEdge(NodeIndex from, NodeIndex to, ubyte ch, TerminalIndex terminal, VarIndex var = VarIndex.max)
1144 	@trusted {
1145 		m_edgeEntries.insert(&m_nodes[from].edges[ch], to);
1146 		addTerminal(to, terminal, var);
1147 	}
1148 
1149 	private NodeIndex addEdge(NodeIndex from, ubyte ch, TerminalIndex terminal, VarIndex var = VarIndex.max)
1150 	@trusted {
1151 		import std.algorithm : canFind;
1152 		import std.string : format;
1153 		if (!m_nodes[from].edges[ch].empty)
1154 			assert(false, format("%s is in %s", ch, m_nodes[from].edges[]));
1155 		auto nidx = addNode();
1156 		addEdge(from, nidx, ch, terminal, var);
1157 		return nidx;
1158 	}
1159 
1160 	private void addTerminal(NodeIndex node, TerminalIndex terminal, VarIndex var)
1161 	@trusted {
1162 		foreach (ref t; m_nodes[node].terminals) {
1163 			if (t.index == terminal) {
1164 				if (t.var != VarIndex.max && t.var != var)
1165 					assert(false, format("Ambiguous route var match!? %s vs %s", t.var, var));
1166 				t.var = var;
1167 				return;
1168 			}
1169 		}
1170 		m_nodes[node].terminals ~= TerminalTag(terminal, var);
1171 	}
1172 
1173 	private NodeIndex addNode()
1174 	@trusted {
1175 		assert(m_nodes.length <= 1_000_000_000, "More than 1B nodes in tree!?");
1176 		auto idx = cast(NodeIndex)m_nodes.length;
1177 		m_nodes ~= Node.init;
1178 		return idx;
1179 	}
1180 }
1181 
1182 
1183 /** Used to store and manipulate multiple linked lists within a single array
1184 	based storage.
1185 */
1186 struct LinkedSetBacking(T) {
1187 	import std.container.array : Array;
1188 	import std.range : isInputRange;
1189 
1190 	static struct Handle {
1191 		uint index = uint.max;
1192 		@property bool empty() const { return index == uint.max; }
1193 	}
1194 
1195 	private {
1196 		static struct Entry {
1197 			uint next;
1198 			T value;
1199 		}
1200 
1201 		Array!Entry m_storage;
1202 
1203 		static struct Range {
1204 			private {
1205 				Array!Entry* backing;
1206 				uint index;
1207 			}
1208 
1209 			@property bool empty() const { return index == uint.max; }
1210 			@property ref const(T) front() const { return (*backing)[index].value; }
1211 
1212 			void popFront()
1213 			{
1214 				index = (*backing)[index].next;
1215 			}
1216 		}
1217 	}
1218 
1219 	@property Handle emptySet() { return Handle.init; }
1220 
1221 	Handle create(scope T[] items...)
1222 	{
1223 		Handle ret;
1224 		foreach (i; items)
1225 			ret.index = createNode(i, ret.index);
1226 		return ret;
1227 	}
1228 
1229 	void insert(Handle* h, T value)
1230 	{
1231 		/*foreach (c; getItems(*h))
1232 			if (value == c)
1233 				return;*/
1234 		h.index = createNode(value, h.index);
1235 	}
1236 
1237 	void insert(R)(Handle* h, R items)
1238 		if (isInputRange!R)
1239 	{
1240 		foreach (itm; items)
1241 			insert(h, itm);
1242 	}
1243 
1244 	LinkedSetHash getHash(Handle sh)
1245 	const {
1246 		import std.digest.md : md5Of;
1247 
1248 		// NOTE: the returned hash is order independent, to avoid bogus
1249 		//       mismatches when comparing lists of different order
1250 		LinkedSetHash ret = cast(LinkedSetHash)md5Of([]);
1251 		while (sh != Handle.init) {
1252 			auto h = cast(LinkedSetHash)md5Of(cast(const(ubyte)[])(&m_storage[sh.index].value)[0 .. 1]);
1253 			foreach (i; 0 .. ret.length) ret[i] ^= h[i];
1254 			sh.index = m_storage[sh.index].next;
1255 		}
1256 		return ret;
1257 	}
1258 
1259 	auto getItems(Handle sh) { return Range(&m_storage, sh.index); }
1260 	auto getItems(Handle sh) const { return Range(&(cast()this).m_storage, sh.index); }
1261 
1262 	bool hasMaxLength(Handle sh, size_t l)
1263 	const {
1264 		uint idx = sh.index;
1265 		do {
1266 			if (idx == uint.max) return true;
1267 			idx = m_storage[idx].next;
1268 		} while (l-- > 0);
1269 		return false;
1270 	}
1271 
1272 	bool hasLength(Handle sh, size_t l)
1273 	const {
1274 		uint idx = sh.index;
1275 		while (l-- > 0) {
1276 			if (idx == uint.max) return false;
1277 			idx = m_storage[idx].next;
1278 		}
1279 		return idx == uint.max;
1280 	}
1281 
1282 	private uint createNode(ref T val, uint next)
1283 	{
1284 		auto id = cast(uint)m_storage.length;
1285 		m_storage ~= Entry(next, val);
1286 		return id;
1287 	}
1288 }
1289 
1290 unittest {
1291 	import std.algorithm.comparison : equal;
1292 
1293 	LinkedSetBacking!int b;
1294 	auto s = b.emptySet;
1295 	assert(s.empty);
1296 	assert(b.getItems(s).empty);
1297 	s = b.create(3, 5, 7);
1298 	assert(b.getItems(s).equal([7, 5, 3]));
1299 	assert(!b.hasLength(s, 2));
1300 	assert(b.hasLength(s, 3));
1301 	assert(!b.hasLength(s, 4));
1302 	assert(!b.hasMaxLength(s, 2));
1303 	assert(b.hasMaxLength(s, 3));
1304 	assert(b.hasMaxLength(s, 4));
1305 
1306 	auto h = b.getHash(s);
1307 	assert(h != b.getHash(b.emptySet));
1308 	s = b.create(5, 3, 7);
1309 	assert(b.getHash(s) == h);
1310 	assert(b.getItems(s).equal([7, 3, 5]));
1311 
1312 	b.insert(&s, 11);
1313 	assert(b.hasLength(s, 4));
1314 	assert(b.getHash(s) != h);
1315 	assert(b.getItems(s).equal([11, 7, 3, 5]));
1316 }
1317 
1318 alias LinkedSetHash = ulong[16/ulong.sizeof];
1319 
1320 private struct Stack(E)
1321 {
1322 	import std.range : isInputRange;
1323 
1324 	private {
1325 		E[] m_storage;
1326 		size_t m_fill;
1327 		debug size_t m_maxFill;
1328 	}
1329 
1330 	@property bool empty() const { return m_fill == 0; }
1331 
1332 	@property size_t fill() const { return m_fill; }
1333 
1334 	debug @property size_t maxSize() const { return m_maxFill; }
1335 
1336 	void reserve(size_t amt)
1337 	{
1338 		auto minsz = m_fill + amt;
1339 		if (m_storage.length < minsz) {
1340 			auto newlength = 64;
1341 			while (newlength < minsz) newlength *= 2;
1342 			m_storage.length = newlength;
1343 		}
1344 	}
1345 
1346 	void push(E el)
1347 	{
1348 		reserve(1);
1349 		m_storage[m_fill++] = el;
1350 		debug if (m_fill > m_maxFill) m_maxFill = m_fill;
1351 	}
1352 
1353 	void push(R)(R els)
1354 		if (isInputRange!R)
1355 	{
1356 		reserve(els.length);
1357 		foreach (el; els)
1358 			m_storage[m_fill++] = el;
1359 		debug if (m_fill > m_maxFill) m_maxFill = m_fill;
1360 	}
1361 
1362 	E pop()
1363 	{
1364 		assert(!empty, "Stack underflow.");
1365 		return m_storage[--m_fill];
1366 	}
1367 }