diff --git a/package.json b/package.json
index d99559f..0be8196 100644
--- a/package.json
+++ b/package.json
@@ -7,9 +7,5 @@
 		"Sönke Ludwig"
 	],
 	"dependencies": {
-		"vibe-d": "~master"
-	},
-	"versions": [
-		"VibeCustomMain"
-	]
+	}
 }
\ No newline at end of file
diff --git a/source/app.d b/source/app.d
index 7eb0307..2cd7afe 100644
--- a/source/app.d
+++ b/source/app.d
@@ -18,11 +18,11 @@
 import vibe.core.file;
 import vibe.core.log;
 import vibe.inet.url;
-import vibe.utils.string;
 
 import std.algorithm;
 import std.array;
 import std.conv;
+import std.encoding;
 import std.exception;
 import std.file;
 import std.getopt;
@@ -247,7 +247,7 @@
 	catch(Throwable e)
 	{
 		logError("Error: %s\n", e.msg);
-		logDebug("Full exception: %s", sanitizeUTF8(cast(ubyte[])e.toString()));
+		logDebug("Full exception: %s", sanitize(e.toString()));
 		logInfo("Run 'dub help' for usage information.");
 		return 1;
 	}
diff --git a/source/dub/generators/monod.d b/source/dub/generators/monod.d
index ee48485..a96996c 100644
--- a/source/dub/generators/monod.d
+++ b/source/dub/generators/monod.d
@@ -97,7 +97,7 @@
 		sln.put("EndGlobal\n");
 	}
 	
-	private void generateSolutionEntry(OutputStream ret, GeneratorSettings settings, const Package pack)
+	private void generateSolutionEntry(RangeFile ret, GeneratorSettings settings, const Package pack)
 	{
 		auto projUuid = generateUUID();
 		auto projName = pack.name;
diff --git a/source/dub/packagemanager.d b/source/dub/packagemanager.d
index 5e5362e..182f96c 100644
--- a/source/dub/packagemanager.d
+++ b/source/dub/packagemanager.d
@@ -22,7 +22,6 @@
 import vibe.core.log;
 import vibe.data.json;
 import vibe.inet.path;
-import vibe.stream.operations;
 
 
 enum JournalJsonFilename = "journal.json";
diff --git a/source/dub/registry.d b/source/dub/registry.d
index 0b47578..643c221 100644
--- a/source/dub/registry.d
+++ b/source/dub/registry.d
Binary files differ
diff --git a/source/dub/utils.d b/source/dub/utils.d
index f6f4fd7..61c30fd 100644
--- a/source/dub/utils.d
+++ b/source/dub/utils.d
@@ -11,8 +11,6 @@
 import vibe.core.log;
 import vibe.data.json;
 import vibe.inet.url;
-import vibe.stream.operations;
-import vibe.utils.string;
 
 // todo: cleanup imports.
 import std.array;
@@ -40,8 +38,8 @@
 
 package Json jsonFromZip(Path zip, string filename) {
 	auto f = openFile(zip, FileMode.Read);
-	ubyte[] b = new ubyte[cast(uint)f.leastSize];
-	f.read(b);
+	ubyte[] b = new ubyte[cast(size_t)f.size];
+	f.rawRead(b);
 	f.close();
 	auto archive = new ZipArchive(b);
 	auto text = stripUTF8Bom(cast(string)archive.expand(archive.directory[filename]));
@@ -64,4 +62,11 @@
 	if( !existsFile(path) ) return false;
 	auto fi = getFileInfo(path);
 	return fi.isDirectory;
-}
\ No newline at end of file
+}
+
+private string stripUTF8Bom(string str)
+{
+	if( str.length >= 3 && str[0 .. 3] == [0xEF, 0xBB, 0xBF] )
+		return str[3 ..$];
+	return str;
+}
diff --git a/source/stdx/process.d b/source/stdx/process.d
new file mode 100644
index 0000000..3dd7c1e
--- /dev/null
+++ b/source/stdx/process.d
@@ -0,0 +1,1419 @@
+// Written in the D programming language.
+
+/** This is a proposal for a replacement for the $(D std._process) module.
+
+    This is a summary of the functions in this module:
+    $(UL $(LI
+        $(LREF spawnProcess) spawns a new _process, optionally assigning it an
+        arbitrary set of standard input, output, and error streams.
+        The function returns immediately, leaving the child _process to execute
+        in parallel with its parent.  All other functions in this module that
+        spawn processes are built around $(LREF spawnProcess).)
+    $(LI
+        $(LREF wait) makes the parent _process wait for a child _process to
+        terminate.  In general one should always do this, to avoid
+        child _processes becoming "zombies" when the parent _process exits.
+        Scope guards are perfect for this – see the $(LREF spawnProcess)
+        documentation for examples.)
+    $(LI
+        $(LREF pipeProcess) and $(LREF pipeShell) also spawn a child _process
+        which runs in parallel with its parent.  However, instead of taking
+        arbitrary streams, they automatically create a set of
+        pipes that allow the parent to communicate with the child
+        through the child's standard input, output, and/or error streams.
+        These functions correspond roughly to C's $(D popen) function.)
+    $(LI
+        $(LREF execute) and $(LREF shell) start a new _process and wait for it
+        to complete before returning.  Additionally, they capture
+        the _process' standard output and error streams and return
+        the output of these as a string.
+        These correspond roughly to C's $(D system) function.)
+    )
+    $(LREF shell) and $(LREF pipeShell) both run the given command
+    through the user's default command interpreter.  On Windows, this is
+    the $(I cmd.exe) program, on POSIX it is determined by the SHELL environment
+    variable (defaulting to $(I /bin/sh) if it cannot be determined).  The
+    command is specified as a single string which is sent directly to the
+    shell.
+
+    The other commands all have two forms, one where the program name
+    and its arguments are specified in a single string parameter, separated
+    by spaces, and one where the arguments are specified as an array of
+    strings.  Use the latter whenever the program name or any of the arguments
+    contain spaces.
+
+    Unless a directory is specified in the program name, all functions will
+    search for the executable in the directories specified in the PATH
+    environment variable.
+
+    Macros:
+    WIKI=Phobos/StdProcess
+*/
+module stdx.process;
+
+
+version(Posix)
+{
+    import core.stdc.errno;
+    import core.stdc.string;
+    import core.sys.posix.stdio;
+    import core.sys.posix.unistd;
+    import core.sys.posix.sys.wait;
+}
+version(Windows)
+{
+    import core.sys.windows.windows;
+    import std.utf;
+    import std.windows.syserror;
+    import core.stdc.stdio;
+    version(DigitalMars)
+    {
+        // this helps on Wine
+        version = PIPE_USE_ALT_FDOPEN;
+    }
+}
+
+import std.algorithm;
+import std.array;
+import std.conv;
+import std.exception;
+import std.path;
+import std.stdio;
+import std.string;
+import std.typecons;
+
+
+version(Posix)
+{
+    version(OSX)
+    {
+        // https://www.gnu.org/software/gnulib/manual/html_node/environ.html
+        private extern(C) char*** _NSGetEnviron();
+        private __gshared char** environ;
+
+        shared static this()
+        {
+            environ = *_NSGetEnviron();
+        }
+    }
+    else
+    {
+        // Made available by the C runtime:
+        private extern(C) extern __gshared char** environ;
+    }
+}
+else version(Windows)
+{
+    // Use the same spawnProcess() implementations on both Windows
+    // and POSIX, only the spawnProcessImpl() function has to be
+    // different.
+    private __gshared LPVOID environ = null;
+
+    extern(System) BOOL TerminateProcess(HANDLE hProcess, UINT uExitCode);
+}
+
+
+
+
+/** A handle corresponding to a spawned process. */
+final class Pid
+{
+    /** The ID number assigned to the process by the operating
+        system.
+    */
+    @property int processID() const
+    {
+        enforce(_processID >= 0,
+            "Pid doesn't correspond to a running process.");
+        return _processID;
+    }
+
+
+    // See module-level wait() for documentation.
+    version(Posix){
+        int wait()
+        {
+            if (_processID == terminated) return _exitCode;
+
+            int exitCode;
+            while(true)
+            {
+                int status;
+                auto check = waitpid(processID, &status, 0);
+                enforce (check != -1  ||  errno != ECHILD,
+                    "Process does not exist or is not a child process.");
+
+                if (WIFEXITED(status))
+                {
+                    exitCode = WEXITSTATUS(status);
+                    break;
+                }
+                else if (WIFSIGNALED(status))
+                {
+                    exitCode = -WTERMSIG(status);
+                    break;
+                }
+                // Process has stopped, but not terminated, so we continue waiting.
+            }
+
+            // Mark Pid as terminated, and cache and return exit code.
+            _processID = terminated;
+            _exitCode = exitCode;
+            return exitCode;
+        }
+
+        bool kill()
+        {
+            return .kill(_processID, SIGKILL) == 0;
+        }
+    }
+    else version(Windows)
+    {
+        int wait()
+        {
+            if (_processID == terminated) return _exitCode;
+
+            if(_handle != INVALID_HANDLE_VALUE)
+            {
+                auto result = WaitForSingleObject(_handle, INFINITE);
+                enforce(result == WAIT_OBJECT_0, "Wait failed");
+                // the process has exited, get the return code
+                enforce(GetExitCodeProcess(_handle, cast(LPDWORD)&_exitCode));
+                CloseHandle(_handle);
+                _handle = INVALID_HANDLE_VALUE;
+                _processID = terminated;
+            }
+            return _exitCode;
+        }
+
+        bool kill()
+        {
+            if(_handle == INVALID_HANDLE_VALUE)
+                return false;
+            return TerminateProcess(_handle, -1) != 0;
+        }
+
+        ~this()
+        {
+            if(_handle != INVALID_HANDLE_VALUE)
+            {
+                CloseHandle(_handle);
+                _handle = INVALID_HANDLE_VALUE;
+            }
+        }
+    }
+
+
+private:
+
+    // Special values for _processID.
+    enum invalid = -1, terminated = -2;
+
+    // OS process ID number.  Only nonnegative IDs correspond to
+    // running processes.
+    int _processID = invalid;
+
+
+    // Exit code cached by wait().  This is only expected to hold a
+    // sensible value if _processID == terminated.
+    int _exitCode;
+
+
+    // Pids are only meant to be constructed inside this module, so
+    // we make the constructor private.
+    version(Windows)
+    {
+        HANDLE _handle;
+        this(int pid, HANDLE handle)
+        {
+            _processID = pid;
+            _handle = handle;
+        }
+    }
+    else
+    {
+        this(int id)
+        {
+            _processID = id;
+        }
+    }
+}
+
+
+
+
+/** Spawns a new process.
+
+    This function returns immediately, and the child process
+    executes in parallel with its parent.
+
+    Unless a directory is specified in the $(D _command) (or $(D name))
+    parameter, this function will search the directories in the
+    PATH environment variable for the program.  To run an executable in
+    the current directory, use $(D "./$(I executable_name)").
+
+    Params:
+        command = A string containing the program name and
+            its arguments, separated by spaces.  If the program
+            name or any of the arguments contain spaces, use
+            the third or fourth form of this function, where
+            they are specified separately.
+
+        environmentVars = The environment variables for the
+            child process can be specified using this parameter.
+            If it is omitted, the child process executes in the
+            same environment as the parent process.
+
+        stdin_ = The standard input stream of the child process.
+            This can be any $(XREF stdio,File) that is opened for reading.
+            By default the child process inherits the parent's input
+            stream.
+
+        stdout_ = The standard output stream of the child process.
+            This can be any $(XREF stdio,File) that is opened for writing.
+            By default the child process inherits the parent's output
+            stream.
+
+        stderr_ = The standard error stream of the child process.
+            This can be any $(XREF stdio,File) that is opened for writing.
+            By default the child process inherits the parent's error
+            stream.
+
+        config = Options controlling the behaviour of $(D spawnProcess).
+            See the $(LREF Config) documentation for details.
+
+        name = The name of the executable file.
+
+        args = The _command line arguments to give to the program.
+            (There is no need to specify the program name as the
+            zeroth argument; this is done automatically.)
+
+    Note:
+    If you pass an $(XREF stdio,File) object that is $(I not) one of the standard
+    input/output/error streams of the parent process, that stream
+    will by default be closed in the parent process when this
+    function returns.  See the $(LREF Config) documentation below for information
+    about how to disable this behaviour.
+
+    Examples:
+    Open Firefox on the D homepage and wait for it to complete:
+    ---
+    auto pid = spawnProcess("firefox http://www.d-programming-language.org");
+    wait(pid);
+    ---
+    Use the $(I ls) _command to retrieve a list of files:
+    ---
+    string[] files;
+    auto p = pipe();
+
+    auto pid = spawnProcess("ls", stdin, p.writeEnd);
+    scope(exit) wait(pid);
+
+    foreach (f; p.readEnd.byLine())  files ~= f.idup;
+    ---
+    Use the $(I ls -l) _command to get a list of files, pipe the output
+    to $(I grep) and let it filter out all files except D source files,
+    and write the output to the file $(I dfiles.txt):
+    ---
+    // Let's emulate the command "ls -l | grep \.d > dfiles.txt"
+    auto p = pipe();
+    auto file = File("dfiles.txt", "w");
+
+    auto lsPid = spawnProcess("ls -l", stdin, p.writeEnd);
+    scope(exit) wait(lsPid);
+
+    auto grPid = spawnProcess("grep \\.d", p.readEnd, file);
+    scope(exit) wait(grPid);
+    ---
+    Open a set of files in OpenOffice Writer, and make it print
+    any error messages to the standard output stream.  Note that since
+    the filenames contain spaces, we have to pass them in an array:
+    ---
+    spawnProcess("oowriter", ["my document.odt", "your document.odt"],
+        stdin, stdout, stdout);
+    ---
+*/
+Pid spawnProcess(string command,
+    File stdin_ = std.stdio.stdin,
+    File stdout_ = std.stdio.stdout,
+    File stderr_ = std.stdio.stderr,
+    Config config = Config.none)
+{
+    auto splitCmd = split(command);
+    return spawnProcessImpl(splitCmd[0], splitCmd[1 .. $],
+        environ,
+        stdin_, stdout_, stderr_, config);
+}
+
+
+/// ditto
+Pid spawnProcess(string command, string[string] environmentVars,
+    File stdin_ = std.stdio.stdin,
+    File stdout_ = std.stdio.stdout,
+    File stderr_ = std.stdio.stderr,
+    Config config = Config.none)
+{
+    auto splitCmd = split(command);
+    return spawnProcessImpl(splitCmd[0], splitCmd[1 .. $],
+        toEnvz(environmentVars),
+        stdin_, stdout_, stderr_, config);
+}
+
+
+/// ditto
+Pid spawnProcess(string name, const string[] args,
+    File stdin_ = std.stdio.stdin,
+    File stdout_ = std.stdio.stdout,
+    File stderr_ = std.stdio.stderr,
+    Config config = Config.none)
+{
+    return spawnProcessImpl(name, args,
+        environ,
+        stdin_, stdout_, stderr_, config);
+}
+
+
+/// ditto
+Pid spawnProcess(string name, const string[] args,
+    string[string] environmentVars,
+    File stdin_ = std.stdio.stdin,
+    File stdout_ = std.stdio.stdout,
+    File stderr_ = std.stdio.stderr,
+    Config config = Config.none)
+{
+    return spawnProcessImpl(name, args,
+        toEnvz(environmentVars),
+        stdin_, stdout_, stderr_, config);
+}
+
+
+// The actual implementation of the above.
+version(Posix) private Pid spawnProcessImpl
+    (string name, const string[] args, const char** envz,
+    File stdin_, File stdout_, File stderr_, Config config)
+{
+    // Make sure the file exists and is executable.
+    if (any!isDirSeparator(name))
+    {
+        enforce(isExecutable(name), "Not an executable file: "~name);
+    }
+    else
+    {
+        name = searchPathFor(name);
+        enforce(name != null, "Executable file not found: "~name);
+    }
+
+    // Get the file descriptors of the streams.
+    auto stdinFD  = core.stdc.stdio.fileno(stdin_.getFP());
+    errnoEnforce(stdinFD != -1, "Invalid stdin stream");
+    auto stdoutFD = core.stdc.stdio.fileno(stdout_.getFP());
+    errnoEnforce(stdoutFD != -1, "Invalid stdout stream");
+    auto stderrFD = core.stdc.stdio.fileno(stderr_.getFP());
+    errnoEnforce(stderrFD != -1, "Invalid stderr stream");
+
+    auto namez = toStringz(name);
+    auto argz = toArgz(name, args);
+
+    auto id = fork();
+    errnoEnforce (id >= 0, "Cannot spawn new process");
+
+    if (id == 0)
+    {
+        // Child process
+
+        // Redirect streams and close the old file descriptors.
+        // In the case that stderr is redirected to stdout, we need
+        // to backup the file descriptor since stdout may be redirected
+        // as well.
+        if (stderrFD == STDOUT_FILENO)  stderrFD = dup(stderrFD);
+        dup2(stdinFD,  STDIN_FILENO);
+        dup2(stdoutFD, STDOUT_FILENO);
+        dup2(stderrFD, STDERR_FILENO);
+
+        // Close the old file descriptors, unless they are
+        // either of the standard streams.
+        if (stdinFD  > STDERR_FILENO)  close(stdinFD);
+        if (stdoutFD > STDERR_FILENO)  close(stdoutFD);
+        if (stderrFD > STDERR_FILENO)  close(stderrFD);
+
+        // Execute program
+        execve(namez, argz, envz);
+
+        // If execution fails, exit as quick as possible.
+        perror("spawnProcess(): Failed to execute program");
+        _exit(1);
+        assert (0);
+    }
+    else
+    {
+        // Parent process:  Close streams and return.
+
+        with (Config)
+        {
+            if (stdinFD  > STDERR_FILENO && !(config & noCloseStdin))
+                stdin_.close();
+            if (stdoutFD > STDERR_FILENO && !(config & noCloseStdout))
+                stdout_.close();
+            if (stderrFD > STDERR_FILENO && !(config & noCloseStderr))
+                stderr_.close();
+        }
+
+        return new Pid(id);
+    }
+}
+else version(Windows) private Pid spawnProcessImpl
+    (string name, const string[] args, LPVOID envz,
+    File stdin_, File stdout_, File stderr_, Config config)
+{
+    // Create a process info structure.  Note that we don't care about wide
+    // characters yet.
+    STARTUPINFO startinfo;
+    startinfo.cb = startinfo.sizeof;
+
+    // Create a process information structure.
+    PROCESS_INFORMATION pi;
+
+    //
+    // Windows is a little strange when passing command line.  It requires the
+    // command-line to be one single command line, and the quoting processing
+    // is rather bizzare.  Through trial and error, here are the rules I've
+    // discovered that Windows uses to parse the command line WRT quotes:
+    //
+    // inside or outside quote mode:
+    // 1. if 2 or more backslashes are followed by a quote, the first
+    //    2 backslashes are reduced to 1 backslash which does not
+    //    affect anything after it.
+    // 2. one backslash followed by a quote is interpreted as a
+    //    literal quote, which cannot be used to close quote mode, and
+    //    does not affect anything after it.
+    //
+    // outside quote mode:
+    // 3. a quote enters quote mode
+    // 4. whitespace delineates an argument
+    //
+    // inside quote mode:
+    // 5. 2 quotes sequentially are interpreted as a literal quote and
+    //    an exit from quote mode.
+    // 6. a quote at the end of the string, or one that is followed by
+    //    anything other than a quote exits quote mode, but does not
+    //    affect the character after the quote.
+    // 7. end of line exits quote mode
+    //
+    // In our 'reverse' routine, we will only utilize the first 2 rules
+    // for escapes.
+    //
+    char[] cmdline;
+    uint minsize = 0;
+    foreach(s; args)
+        minsize += args.length;
+
+    // reserve enough space to hold the program and all the arguments, plus 3
+    // extra characters per arg for the quotes and the space, plus 5 extra
+    // chars for good measure (in case we have to add escaped quotes).
+    cmdline.reserve(minsize + name.length + 3 * args.length + 5);
+
+    // this could be written more optimized...
+    void addArg(string a)
+    {
+        if(cmdline.length)
+            cmdline ~= " ";
+        // first, determine if we need a quote
+        bool needquote = false;
+        foreach(dchar d; a)
+            if(d == ' ')
+            {
+                needquote = true;
+                break;
+            }
+        if(needquote)
+            cmdline ~= '"';
+        foreach(dchar d; a)
+        {
+            if(d == '"')
+                cmdline ~= '\\';
+            cmdline ~= d;
+        }
+        if(needquote)
+            cmdline ~= '"';
+    }
+
+    addArg(name);
+    foreach(a; args)
+        addArg(a);
+
+    cmdline ~= '\0';
+
+    // ok, the command line is ready.  Figure out the startup info
+    startinfo.dwFlags = STARTF_USESTDHANDLES;
+    // Get the file descriptors of the streams.
+    auto stdinFD  = _fileno(stdin_.getFP());
+    errnoEnforce(stdinFD != -1, "Invalid stdin stream");
+    auto stdoutFD = _fileno(stdout_.getFP());
+    errnoEnforce(stdoutFD != -1, "Invalid stdout stream");
+    auto stderrFD = _fileno(stderr_.getFP());
+    errnoEnforce(stderrFD != -1, "Invalid stderr stream");
+
+    // need to convert file descriptors to HANDLEs
+    startinfo.hStdInput = _fdToHandle(stdinFD);
+    startinfo.hStdOutput = _fdToHandle(stdoutFD);
+    startinfo.hStdError = _fdToHandle(stderrFD);
+
+    // TODO: need to fix this for unicode
+    if(!CreateProcessA(null, cmdline.ptr, null, null, true, (config & Config.gui) ? CREATE_NO_WINDOW : 0, envz, null, &startinfo, &pi))
+    {
+        throw new Exception("Error starting process: " ~ sysErrorString(GetLastError()), __FILE__, __LINE__);
+    }
+
+    // figure out if we should close any of the streams
+    with (Config)
+    {
+        if (stdinFD  > STDERR_FILENO && !(config & noCloseStdin))
+            stdin_.close();
+        if (stdoutFD > STDERR_FILENO && !(config & noCloseStdout))
+            stdout_.close();
+        if (stderrFD > STDERR_FILENO && !(config & noCloseStderr))
+            stderr_.close();
+    }
+
+    // close the thread handle in the process info structure
+    CloseHandle(pi.hThread);
+
+    return new Pid(pi.dwProcessId, pi.hProcess);
+}
+
+// Searches the PATH variable for the given executable file,
+// (checking that it is in fact executable).
+version(Posix) private string searchPathFor(string executable)
+{
+    auto pathz = environment["PATH"];
+    if (pathz == null)  return null;
+
+    foreach (dir; splitter(to!string(pathz), ':'))
+    {
+        auto execPath = buildPath(dir, executable);
+        if (isExecutable(execPath))  return execPath;
+    }
+
+    return null;
+}
+
+// Converts a C array of C strings to a string[] array,
+// setting the program name as the zeroth element.
+version(Posix) private const(char)** toArgz(string prog, const string[] args)
+{
+    alias const(char)* stringz_t;
+    auto argz = new stringz_t[](args.length+2);
+
+    argz[0] = toStringz(prog);
+    foreach (i; 0 .. args.length)
+    {
+        argz[i+1] = toStringz(args[i]);
+    }
+    argz[$-1] = null;
+    return argz.ptr;
+}
+
+// Converts a string[string] array to a C array of C strings
+// on the form "key=value".
+version(Posix) private const(char)** toEnvz(const string[string] env)
+{
+    alias const(char)* stringz_t;
+    auto envz = new stringz_t[](env.length+1);
+    int i = 0;
+    foreach (k, v; env)
+    {
+        envz[i] = (k~'='~v~'\0').ptr;
+        i++;
+    }
+    envz[$-1] = null;
+    return envz.ptr;
+}
+else version(Windows) private LPVOID toEnvz(const string[string] env)
+{
+    uint len = 1; // reserve 1 byte for termination of environment block
+    foreach(k, v; env)
+    {
+        len += k.length + v.length + 2; // one for '=', one for null char
+    }
+
+    char [] envz;
+    envz.reserve(len);
+    foreach(k, v; env)
+    {
+        envz ~= k ~ '=' ~ v ~ '\0';
+    }
+
+    envz ~= '\0';
+    return envz.ptr;
+}
+
+
+// Checks whether the file exists and can be executed by the
+// current user.
+version(Posix) private bool isExecutable(string path)
+{
+    return (access(toStringz(path), X_OK) == 0);
+}
+
+
+
+
+/** Flags that control the behaviour of $(LREF spawnProcess).
+    Use bitwise OR to combine flags.
+
+    Example:
+    ---
+    auto logFile = File("myapp_error.log", "w");
+
+    // Start program in a console window (Windows only), redirect
+    // its error stream to logFile, and leave logFile open in the
+    // parent process as well.
+    auto pid = spawnProcess("myapp", stdin, stdout, logFile,
+        Config.noCloseStderr | Config.gui);
+    scope(exit)
+    {
+        auto exitCode = wait(pid);
+        logFile.writeln("myapp exited with code ", exitCode);
+        logFile.close();
+    }
+    ---
+*/
+enum Config
+{
+    none = 0,
+
+    /** Unless the child process inherits the standard
+        input/output/error streams of its parent, one almost
+        always wants the streams closed in the parent when
+        $(LREF spawnProcess) returns.  Therefore, by default, this
+        is done.  If this is not desirable, pass any of these
+        options to spawnProcess.
+    */
+    noCloseStdin  = 1,
+    noCloseStdout = 2,                                  /// ditto
+    noCloseStderr = 4,                                  /// ditto
+
+    /** On Windows, this option causes the process to run in
+        a console window.  On POSIX it has no effect.
+    */
+    gui = 8,
+}
+
+
+
+
+/** Waits for a specific spawned process to terminate and returns
+    its exit status.
+
+    In general one should always _wait for child processes to terminate
+    before exiting the parent process.  Otherwise, they may become
+    "$(WEB en.wikipedia.org/wiki/Zombie_process,zombies)" – processes
+    that are defunct, yet still occupy a slot in the OS process table.
+
+    Note:
+    On POSIX systems, if the process is terminated by a signal,
+    this function returns a negative number whose absolute value
+    is the signal number.  (POSIX restricts normal exit codes
+    to the range 0-255.)
+
+    Examples:
+    See the $(LREF spawnProcess) documentation.
+*/
+int wait(Pid pid)
+{
+    enforce(pid !is null, "Called wait on a null Pid.");
+    return pid.wait();
+}
+
+
+// BIG HACK: works around the use of a private File() contrctor in pipe()
+private File encapPipeAsFile(FILE* fil)
+{
+    static struct Impl
+    {
+        FILE * handle = null; // Is null iff this Impl is closed by another File
+        uint refs = uint.max / 2;
+        bool isPipe;
+    }
+    auto f = File.wrapFile(fil);
+    auto imp = *cast(Impl**)&f;
+    imp.refs = 1;
+    imp.isPipe = true;
+    return f;
+} 
+
+/** Creates a unidirectional _pipe.
+
+    Data is written to one end of the _pipe and read from the other.
+    ---
+    auto p = pipe();
+    p.writeEnd.writeln("Hello World");
+    assert (p.readEnd.readln().chomp() == "Hello World");
+    ---
+    Pipes can, for example, be used for interprocess communication
+    by spawning a new process and passing one end of the _pipe to
+    the child, while the parent uses the other end.  See the
+    $(LREF spawnProcess) documentation for examples of this.
+*/
+version(Posix) Pipe pipe()
+{
+    int[2] fds;
+    errnoEnforce(core.sys.posix.unistd.pipe(fds) == 0,
+                 "Unable to create pipe");
+
+    Pipe p;
+
+    p._read = encapPipeAsFile(errnoEnforce(fdopen(fds[0], "r"), "Cannot open read end of pipe"));
+    p._write = encapPipeAsFile(errnoEnforce(fdopen(fds[1], "w"), "Cannot open write end of pipe"));
+
+    return p;
+}
+else version(Windows) Pipe pipe()
+{
+    // use CreatePipe to create an anonymous pipe
+    HANDLE readHandle;
+    HANDLE writeHandle;
+    SECURITY_ATTRIBUTES sa;
+    sa.nLength = sa.sizeof;
+    sa.lpSecurityDescriptor = null;
+    sa.bInheritHandle = true;
+    if(!CreatePipe(&readHandle, &writeHandle, &sa, 0))
+    {
+        throw new Exception("Error creating pipe: " ~ sysErrorString(GetLastError()), __FILE__, __LINE__);
+    }
+
+    // Create file descriptors from the handles
+    auto readfd = _handleToFD(readHandle, FHND_DEVICE);
+    auto writefd = _handleToFD(writeHandle, FHND_DEVICE);
+
+    Pipe p;
+    version(PIPE_USE_ALT_FDOPEN)
+    {
+        // This is a re-implementation of DMC's fdopen, but without the
+        // mucking with the file descriptor.  POSIX standard requires the
+        // new fdopen'd file to retain the given file descriptor's
+        // position.
+        FILE * local_fdopen(int fd, const(char)* mode)
+        {
+            auto fp = core.stdc.stdio.fopen("NUL", mode);
+            if(!fp)
+                return null;
+            FLOCK(fp);
+            auto iob = cast(_iobuf*)fp;
+            .close(iob._file);
+            iob._file = fd;
+            iob._flag &= ~_IOTRAN;
+            FUNLOCK(fp);
+            return fp;
+        }
+
+        p._read = encapPipeAsFile(errnoEnforce(local_fdopen(readfd, "r"), "Cannot open read end of pipe"));
+        p._write = encapPipeAsFile(errnoEnforce(local_fdopen(writefd, "a"), "Cannot open write end of pipe"));
+    }
+    else
+    {
+        p._read = encapPipeAsFile(errnoEnforce(fdopen(readfd, "r"), "Cannot open read end of pipe"));
+        p._write = encapPipeAsFile(errnoEnforce(fdopen(writefd, "a"), "Cannot open write end of pipe"));
+    }
+
+    return p;
+}
+
+
+/// ditto
+struct Pipe
+{
+    /** The read end of the pipe. */
+    @property File readEnd() { return _read; }
+
+
+    /** The write end of the pipe. */
+    @property File writeEnd() { return _write; }
+
+
+    /** Closes both ends of the pipe.
+
+        Normally it is not necessary to do this manually, as $(XREF stdio,File)
+        objects are automatically closed when there are no more references
+        to them.
+
+        Note that if either end of the pipe has been passed to a child process,
+        it will only be closed in the parent process.
+    */
+    void close()
+    {
+        _read.close();
+        _write.close();
+    }
+
+
+private:
+    File _read, _write;
+}
+
+
+/*unittest
+{
+    auto p = pipe();
+    p.writeEnd.writeln("Hello World");
+    p.writeEnd.flush();
+    assert (p.readEnd.readln().chomp() == "Hello World");
+}*/
+
+
+
+
+// ============================== pipeProcess() ==============================
+
+
+/** Starts a new process, creating pipes to redirect its standard
+    input, output and/or error streams.
+
+    These functions return immediately, leaving the child process to
+    execute in parallel with the parent.
+    $(LREF pipeShell) invokes the user's _command interpreter
+    to execute the given program or _command.
+
+    Example:
+    ---
+    auto pipes = pipeProcess("my_application");
+
+    // Store lines of output.
+    string[] output;
+    foreach (line; pipes.stdout.byLine) output ~= line.idup;
+
+    // Store lines of errors.
+    string[] errors;
+    foreach (line; pipes.stderr.byLine) errors ~= line.idup;
+    ---
+*/
+ProcessPipes pipeProcess(string command,
+    Redirect redirectFlags = Redirect.all)
+{
+    auto splitCmd = split(command);
+    return pipeProcess(splitCmd[0], splitCmd[1 .. $], redirectFlags);
+}
+
+
+/// ditto
+ProcessPipes pipeProcess(string name, string[] args,
+    Redirect redirectFlags = Redirect.all)
+{
+    File stdinFile, stdoutFile, stderrFile;
+
+    ProcessPipes pipes;
+    pipes._redirectFlags = redirectFlags;
+
+    if (redirectFlags & Redirect.stdin)
+    {
+        auto p = pipe();
+        stdinFile = p.readEnd;
+        pipes._stdin = p.writeEnd;
+    }
+    else
+    {
+        stdinFile = std.stdio.stdin;
+    }
+
+    if (redirectFlags & Redirect.stdout)
+    {
+        enforce((redirectFlags & Redirect.stdoutToStderr) == 0,
+            "Invalid combination of options: Redirect.stdout | "
+           ~"Redirect.stdoutToStderr");
+        auto p = pipe();
+        stdoutFile = p.writeEnd;
+        pipes._stdout = p.readEnd;
+    }
+    else
+    {
+        stdoutFile = std.stdio.stdout;
+    }
+
+    if (redirectFlags & Redirect.stderr)
+    {
+        enforce((redirectFlags & Redirect.stderrToStdout) == 0,
+            "Invalid combination of options: Redirect.stderr | "
+           ~"Redirect.stderrToStdout");
+        auto p = pipe();
+        stderrFile = p.writeEnd;
+        pipes._stderr = p.readEnd;
+    }
+    else
+    {
+        stderrFile = std.stdio.stderr;
+    }
+
+    if (redirectFlags & Redirect.stdoutToStderr)
+    {
+        if (redirectFlags & Redirect.stderrToStdout)
+        {
+            // We know that neither of the other options have been
+            // set, so we assign the std.stdio.std* streams directly.
+            stdoutFile = std.stdio.stderr;
+            stderrFile = std.stdio.stdout;
+        }
+        else
+        {
+            stdoutFile = stderrFile;
+        }
+    }
+    else if (redirectFlags & Redirect.stderrToStdout)
+    {
+        stderrFile = stdoutFile;
+    }
+
+    pipes._pid = spawnProcess(name, args, stdinFile, stdoutFile, stderrFile);
+    return pipes;
+}
+
+
+/// ditto
+ProcessPipes pipeShell(string command, Redirect redirectFlags = Redirect.all)
+{
+    return pipeProcess(getShell(), [shellSwitch, command], redirectFlags);
+}
+
+
+
+
+/** Flags that can be passed to $(LREF pipeProcess) and $(LREF pipeShell)
+    to specify which of the child process' standard streams are redirected.
+    Use bitwise OR to combine flags.
+*/
+enum Redirect
+{
+    none = 0,
+
+    /** Redirect the standard input, output or error streams, respectively. */
+    stdin = 1,
+    stdout = 2,                             /// ditto
+    stderr = 4,                             /// ditto
+    all = stdin | stdout | stderr,          /// ditto
+
+    /** Redirect the standard error stream into the standard output
+        stream, and vice versa.
+    */
+    stderrToStdout = 8,
+    stdoutToStderr = 16,                    /// ditto
+}
+
+
+
+
+/** Object containing $(XREF stdio,File) handles that allow communication with
+    a child process through its standard streams.
+*/
+struct ProcessPipes
+{
+    /** Returns the $(LREF Pid) of the child process. */
+    @property Pid pid()
+    {
+        enforce (_pid !is null);
+        return _pid;
+    }
+
+
+    /** Returns an $(XREF stdio,File) that allows writing to the child process'
+        standard input stream.
+    */
+    @property File stdin()
+    {
+        enforce ((_redirectFlags & Redirect.stdin) > 0,
+            "Child process' standard input stream hasn't been redirected.");
+        return _stdin;
+    }
+
+
+    /** Returns an $(XREF stdio,File) that allows reading from the child
+        process' standard output/error stream.
+    */
+    @property File stdout()
+    {
+        enforce ((_redirectFlags & Redirect.stdout) > 0,
+            "Child process' standard output stream hasn't been redirected.");
+        return _stdout;
+    }
+
+    /// ditto
+    @property File stderr()
+    {
+        enforce ((_redirectFlags & Redirect.stderr) > 0,
+            "Child process' standard error stream hasn't been redirected.");
+        return _stderr;
+    }
+
+
+private:
+
+    Redirect _redirectFlags;
+    Pid _pid;
+    File _stdin, _stdout, _stderr;
+}
+
+
+
+
+// ============================== execute() ==============================
+
+
+/** Executes the given program and returns its exit code and output.
+
+    This function blocks until the program terminates.
+    The $(D output) string includes what the program writes to its
+    standard error stream as well as its standard output stream.
+    ---
+    auto dmd = execute("dmd myapp.d");
+    if (dmd.status != 0) writeln("Compilation failed:\n", dmd.output);
+    ---
+*/
+Tuple!(int, "status", string, "output") execute(string command)
+{
+    auto p = pipeProcess(command,
+        Redirect.stdout | Redirect.stderrToStdout);
+
+    Appender!(ubyte[]) a;
+    foreach (ubyte[] chunk; p.stdout.byChunk(4096))  a.put(chunk);
+
+    typeof(return) r;
+    r.output = cast(string) a.data;
+    r.status = wait(p.pid);
+    return r;
+}
+
+
+/// ditto
+Tuple!(int, "status", string, "output") execute(string name, string[] args...)
+{
+    auto p = pipeProcess(name, args,
+        Redirect.stdout | Redirect.stderrToStdout);
+
+    Appender!(ubyte[]) a;
+    foreach (ubyte[] chunk; p.stdout.byChunk(4096))  a.put(chunk);
+
+    typeof(return) r;
+    r.output = cast(string) a.data;
+    r.status = wait(p.pid);
+    return r;
+}
+
+
+
+
+// ============================== shell() ==============================
+
+
+version(Posix)   private immutable string shellSwitch = "-c";
+version(Windows) private immutable string shellSwitch = "/C";
+
+
+// Gets the user's default shell.
+version(Posix)  private string getShell()
+{
+    return environment.get("SHELL", "/bin/sh");
+}
+
+version(Windows) private string getShell()
+{
+    return "cmd.exe";
+}
+
+
+
+
+/** Executes $(D _command) in the user's default _shell and returns its
+    exit code and output.
+
+    This function blocks until the command terminates.
+    The $(D output) string includes what the command writes to its
+    standard error stream as well as its standard output stream.
+    ---
+    auto ls = shell("ls -l");
+    writefln("ls exited with code %s and said: %s", ls.status, ls.output);
+    ---
+*/
+Tuple!(int, "status", string, "output") shell(string command)
+{
+    version(Windows)
+        return execute(getShell() ~ " " ~ shellSwitch ~ " " ~ command);
+    else version(Posix)
+        return execute(getShell(), shellSwitch, command);
+    else assert(0);
+}
+
+
+
+
+// ============================== thisProcessID ==============================
+
+
+/** Returns the process ID number of the current process. */
+version(Posix) @property int thisProcessID()
+{
+    return getpid();
+}
+
+version(Windows) @property int thisProcessID()
+{
+    return GetCurrentProcessId();
+}
+
+
+
+
+// ============================== environment ==============================
+
+
+/** Manipulates environment variables using an associative-array-like
+    interface.
+
+    Examples:
+    ---
+    // Return variable, or throw an exception if it doesn't exist.
+    string path = environment["PATH"];
+
+    // Add/replace variable.
+    environment["foo"] = "bar";
+
+    // Remove variable.
+    environment.remove("foo");
+
+    // Return variable, or null if it doesn't exist.
+    string foo = environment.get("foo");
+
+    // Return variable, or a default value if it doesn't exist.
+    string foo = environment.get("foo", "default foo value");
+
+    // Return an associative array containing all the environment variables.
+    string[string] aa = environment.toAA();
+    ---
+*/
+alias Environment environment;
+
+abstract final class Environment
+{
+static:
+
+    // Retrieves an environment variable, throws on failure.
+    string opIndex(string name)
+    {
+        string value;
+        enforce(getImpl(name, value), "Environment variable not found: "~name);
+        return value;
+    }
+
+
+
+    // Assigns a value to an environment variable.  If the variable
+    // exists, it is overwritten.
+    string opIndexAssign(string value, string name)
+    {
+        version(Posix)
+        {
+            if (core.sys.posix.stdlib.setenv(toStringz(name),
+                toStringz(value), 1) != -1)
+            {
+                return value;
+            }
+
+            // The default errno error message is very uninformative
+            // in the most common case, so we handle it manually.
+            enforce(errno != EINVAL,
+                "Invalid environment variable name: '"~name~"'");
+            errnoEnforce(false,
+                "Failed to add environment variable");
+            assert(0);
+        }
+
+        else version(Windows)
+        {
+            enforce(
+                SetEnvironmentVariableW(toUTF16z(name), toUTF16z(value)),
+                sysErrorString(GetLastError())
+            );
+            return value;
+        }
+
+        else static assert(0);
+    }
+
+
+
+    // Removes an environment variable.  The function succeeds even
+    // if the variable isn't in the environment.
+    void remove(string name)
+    {
+        version(Posix)
+        {
+            core.sys.posix.stdlib.unsetenv(toStringz(name));
+        }
+
+        else version(Windows)
+        {
+            SetEnvironmentVariableW(toUTF16z(name), null);
+        }
+
+        else static assert(0);
+    }
+
+
+
+    // Same as opIndex, except it returns a default value if
+    // the variable doesn't exist.
+    string get(string name, string defaultValue = null)
+    {
+        string value;
+        auto found = getImpl(name, value);
+        return found ? value : defaultValue;
+    }
+
+
+
+    // Returns all environment variables in an associative array.
+    // Environment variable of zero-length is not retrieved.
+    string[string] toAA()
+    {
+        string[string] aa;
+
+        version(Posix)
+        {
+            for (int i=0; environ[i] != null; ++i)
+            {
+                immutable varDef = to!string(environ[i]);
+                immutable eq = std.string.indexOf(varDef, '=');
+                assert (eq >= 0);
+
+                immutable name = varDef[0 .. eq];
+                if (!name.length)
+                    continue;
+
+                immutable value = varDef[eq+1 .. $];
+
+                // In POSIX, environment variables may be defined more
+                // than once.  This is a security issue, which we avoid
+                // by checking whether the key already exists in the array.
+                // For more info:
+                // http://www.dwheeler.com/secure-programs/Secure-Programs-HOWTO/environment-variables.html
+                if (name !in aa)  aa[name] = value;
+            }
+        }
+        else version(Windows)
+        {
+            auto envBlock = GetEnvironmentStringsW();
+            enforce (envBlock, "Failed to retrieve environment variables.");
+            scope(exit) FreeEnvironmentStringsW(envBlock);
+
+            for (int i=0; envBlock[i] != '\0'; ++i)
+            {
+                auto start = i;
+                while (envBlock[i] != '=')
+                {
+                    assert (envBlock[i] != '\0');
+                    ++i;
+                }
+                immutable name = toUTF8(envBlock[start .. i]);
+
+                start = i+1;
+                while (envBlock[i] != '\0') ++i;
+                if (name.length)
+                    aa[name] = toUTF8(envBlock[start .. i]);
+            }
+        }
+
+        else static assert(0);
+
+        return aa;
+    }
+
+
+private:
+
+    // Returns the length of an environment variable (in number of
+    // wchars, including the null terminator), or 0 if it doesn't exist.
+    version(Windows)
+    int varLength(LPCWSTR namez)
+    {
+        return GetEnvironmentVariableW(namez, null, 0);
+    }
+
+
+    // Retrieves the environment variable, returns false on failure.
+    // Environment variable with zero-length name sets an empty value.
+    bool getImpl(string name, out string value)
+    {
+        if (!name.length)
+            return true;  // return empty
+
+        version(Posix)
+        {
+            const vz = core.sys.posix.stdlib.getenv(toStringz(name));
+            if (vz == null) return false;
+            auto v = vz[0 .. strlen(vz)];
+
+            // Cache the last call's result.
+            static string lastResult;
+            if (v != lastResult) lastResult = v.idup;
+            value = lastResult;
+            return true;
+        }
+
+        else version(Windows)
+        {
+            const namez = toUTF16z(name);
+            immutable len = varLength(namez);
+            if (len == 0) return false;
+            if (len == 1) return true;
+
+            auto buf = new WCHAR[len];
+            GetEnvironmentVariableW(namez, buf.ptr, buf.length);
+            value = toUTF8(buf[0 .. $-1]);
+            return true;
+        }
+
+        else static assert(0);
+    }
+}
+
+
+/*unittest
+{
+    // New variable
+    environment["std_process"] = "foo";
+    assert (environment["std_process"] == "foo");
+
+    // Set variable again
+    environment["std_process"] = "bar";
+    assert (environment["std_process"] == "bar");
+
+    // Remove variable
+    environment.remove("std_process");
+
+    // Remove again, should succeed
+    environment.remove("std_process");
+
+    // Throw on not found.
+    try { environment["std_process"]; assert(0); } catch(Exception e) { }
+
+    // get() without default value
+    assert (environment.get("std.process") == null);
+
+    // get() with default value
+    assert (environment.get("std_process", "baz") == "baz");
+
+    // Convert to associative array
+    auto aa = environment.toAA();
+    assert (aa.length > 0);
+    foreach (n, v; aa)
+    {
+        // Wine has some bugs related to environment variables:
+        //  - Wine allows the existence of an env. variable with the name
+        //    "\0", but GetEnvironmentVariable refuses to retrieve it.
+        //  - If an env. variable has zero length, i.e. is "\0",
+        //    GetEnvironmentVariable should return 1.  Instead it returns
+        //    0, indicating the variable doesn't exist.
+        version(Windows)  if (n.length == 0 || v.length == 0) continue;
+
+        import std.string;
+        auto rhs = environment[n];
+        assert (v == rhs, format("key %s -- '%s' != '%s'", n, v, rhs));
+    }
+}*/
diff --git a/source/vibe/core/file.d b/source/vibe/core/file.d
new file mode 100644
index 0000000..3f8b7c6
--- /dev/null
+++ b/source/vibe/core/file.d
@@ -0,0 +1,291 @@
+/**
+	File handling.
+
+	Copyright: © 2012 RejectedSoftware e.K.
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Sönke Ludwig
+*/
+module vibe.core.file;
+
+public import vibe.inet.url;
+public import std.stdio;
+
+import vibe.core.log;
+
+import std.conv;
+import std.c.stdio;
+import std.datetime;
+import std.exception;
+import std.file;
+import std.path;
+import std.string;
+import std.utf;
+
+
+version(Posix){
+	private extern(C) int mkstemps(char* templ, int suffixlen);
+}
+
+
+/* Add output range support to File
+*/
+struct RangeFile {
+	File file;
+	alias file this;
+	void put(in char[] str) { file.write(str); }
+	void put(char ch) { file.write(cast(ubyte)ch); }
+	void put(dchar ch) { char[4] chars; put(chars[0 .. encode(chars, ch)]); }
+	
+	ubyte[] readAll()
+	{
+		auto sz = file.size;
+		enforce(sz <= size_t.max, "File is too big to read to memory.");
+		auto ret = new ubyte[cast(size_t)sz];
+		return file.rawRead(ret);
+	}
+}
+
+
+/**
+	Opens a file stream with the specified mode.
+*/
+RangeFile openFile(Path path, FileMode mode = FileMode.Read)
+{
+	string strmode;
+	final switch(mode){
+		case FileMode.Read: strmode = "rb"; break;
+		case FileMode.ReadWrite: strmode = "rb+"; break;
+		case FileMode.CreateTrunc: strmode = "wb+"; break;
+		case FileMode.Append: strmode = "ab"; break;
+	}
+	return RangeFile(File(path.toNativeString(), strmode));
+}
+/// ditto
+RangeFile openFile(string path, FileMode mode = FileMode.Read)
+{
+	return openFile(Path(path), mode);
+}
+
+/**
+	Creates and opens a temporary file for writing.
+*/
+RangeFile createTempFile(string suffix = null)
+{
+	version(Windows){
+		char[L_tmpnam] tmp;
+		tmpnam(tmp.ptr);
+		auto tmpname = to!string(tmp.ptr);
+		if( tmpname.startsWith("\\") ) tmpname = tmpname[1 .. $];
+		tmpname ~= suffix;
+		logDebug("tmp %s", tmpname);
+		return openFile(tmpname, FileMode.CreateTrunc);
+	} else {
+		enum pattern ="/tmp/vtmp.XXXXXX";
+		scope templ = new char[pattern.length+suffix.length+1];
+		templ[0 .. pattern.length] = pattern;
+		templ[pattern.length .. $-1] = suffix;
+		templ[$-1] = '\0';
+		assert(suffix.length <= int.max);
+		auto fd = mkstemps(templ.ptr, cast(int)suffix.length);
+		enforce(fd >= 0, "Failed to create temporary file.");
+		return File.wrapFile(fdopen(fd));
+	}
+}
+
+/**
+	Moves or renames a file.
+*/
+void moveFile(Path from, Path to)
+{
+	moveFile(from.toNativeString(), to.toNativeString());
+}
+/// ditto
+void moveFile(string from, string to)
+{
+	std.file.rename(from, to);
+}
+
+/**
+	Copies a file.
+
+	Note that attributes and time stamps are currently not retained.
+
+	Params:
+		from = Path of the source file
+		to = Path for the destination file
+		overwrite = If true, any file existing at the destination path will be
+			overwritten. If this is false, an excpetion will be thrown should
+			a file already exist at the destination path.
+
+	Throws:
+		An Exception if the copy operation fails for some reason.
+*/
+void copyFile(Path from, Path to, bool overwrite = false)
+{
+	{
+		auto src = openFile(from, FileMode.Read);
+		scope(exit) src.close();
+		enforce(overwrite || !existsFile(to), "Destination file already exists.");
+		auto dst = openFile(to, FileMode.CreateTrunc);
+		scope(exit) dst.close();
+		dst.write(src);
+	}
+
+	// TODO: retain attributes and time stamps
+}
+/// ditto
+void copyFile(string from, string to)
+{
+	copyFile(Path(from), Path(to));
+}
+
+/**
+	Removes a file
+*/
+void removeFile(Path path)
+{
+	removeFile(path.toNativeString());
+}
+/// ditto
+void removeFile(string path) {
+	std.file.remove(path);
+}
+
+/**
+	Checks if a file exists
+*/
+bool existsFile(Path path) {
+	return existsFile(path.toNativeString());
+}
+/// ditto
+bool existsFile(string path)
+{
+	return std.file.exists(path);
+}
+
+/** Stores information about the specified file/directory into 'info'
+
+	Returns false if the file does not exist.
+*/
+FileInfo getFileInfo(Path path)
+{
+	auto ent = std.file.dirEntry(path.toNativeString());
+	return makeFileInfo(ent);
+}
+/// ditto
+FileInfo getFileInfo(string path)
+{
+	return getFileInfo(Path(path));
+}
+
+/**
+	Creates a new directory.
+*/
+void createDirectory(Path path)
+{
+	mkdir(path.toNativeString());
+}
+/// ditto
+void createDirectory(string path)
+{
+	createDirectory(Path(path));
+}
+
+/**
+	Enumerates all files in the specified directory.
+*/
+void listDirectory(Path path, scope bool delegate(FileInfo info) del)
+{
+	foreach( DirEntry ent; dirEntries(path.toNativeString(), SpanMode.shallow) )
+		if( !del(makeFileInfo(ent)) )
+			break;
+}
+/// ditto
+void listDirectory(string path, scope bool delegate(FileInfo info) del)
+{
+	listDirectory(Path(path), del);
+}
+/// ditto
+int delegate(scope int delegate(ref FileInfo)) iterateDirectory(Path path)
+{
+	int iterator(scope int delegate(ref FileInfo) del){
+		int ret = 0;
+		listDirectory(path, (fi){
+			ret = del(fi);
+			return ret == 0;
+		});
+		return ret;
+	}
+	return &iterator;
+}
+/// ditto
+int delegate(scope int delegate(ref FileInfo)) iterateDirectory(string path)
+{
+	return iterateDirectory(Path(path));
+}
+
+
+/**
+	Returns the current working directory.
+*/
+Path getWorkingDirectory()
+{
+	return Path(std.file.getcwd());
+}
+
+
+/** Contains general information about a file.
+*/
+struct FileInfo {
+	/// Name of the file (not including the path)
+	string name;
+
+	/// Size of the file (zero for directories)
+	ulong size;
+
+	/// Time of the last modification
+	SysTime timeModified;
+
+	/// Time of creation (not available on all operating systems/file systems)
+	SysTime timeCreated;
+
+	/// True if this is a symlink to an actual file
+	bool isSymlink;
+
+	/// True if this is a directory or a symlink pointing to a directory
+	bool isDirectory;
+}
+
+/**
+	Specifies how a file is manipulated on disk.
+*/
+enum FileMode {
+	/// The file is opened read-only.
+	Read,
+	/// The file is opened for read-write random access.
+	ReadWrite,
+	/// The file is truncated if it exists and created otherwise and the opened for read-write access.
+	CreateTrunc,
+	/// The file is opened for appending data to it and created if it does not exist.
+	Append
+}
+
+/**
+	Accesses the contents of a file as a stream.
+*/
+
+private FileInfo makeFileInfo(DirEntry ent)
+{
+	FileInfo ret;
+	ret.name = baseName(ent.name);
+	if( ret.name.length == 0 ) ret.name = ent.name;
+	assert(ret.name.length > 0);
+	ret.size = ent.size;
+	ret.timeModified = ent.timeLastModified;
+	version(Windows) ret.timeCreated = ent.timeCreated;
+	else ret.timeCreated = ent.timeLastModified;
+	ret.isSymlink = ent.isSymlink;
+	ret.isDirectory = ent.isDir;
+	return ret;
+}
+
diff --git a/source/vibe/core/log.d b/source/vibe/core/log.d
new file mode 100644
index 0000000..00579e8
--- /dev/null
+++ b/source/vibe/core/log.d
@@ -0,0 +1,97 @@
+/**
+	Central logging facility for vibe.
+
+	Copyright: © 2012 RejectedSoftware e.K.
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Sönke Ludwig
+*/
+module vibe.core.log;
+
+import std.array;
+import std.datetime;
+import std.format;
+import std.stdio;
+import core.thread;
+
+private {
+	shared LogLevel s_minLevel = LogLevel.Info;
+	shared LogLevel s_logFileLevel;
+	shared bool s_plainLogging = false;
+}
+
+/// Sets the minimum log level to be printed.
+void setLogLevel(LogLevel level) nothrow
+{
+	s_minLevel = level;
+}
+
+/// Disables output of thread/task ids with each log message
+void setPlainLogging(bool enable)
+{
+	s_plainLogging = enable;
+}
+
+/**
+	Logs a message.
+
+	Params:
+		level = The log level for the logged message
+		fmt = See http://dlang.org/phobos/std_format.html#format-string
+*/
+void logTrace(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Trace, fmt, args); }
+/// ditto
+void logDebug(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Debug, fmt, args); }
+/// ditto
+void logInfo(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Info, fmt, args); }
+/// ditto
+void logWarn(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Warn, fmt, args); }
+/// ditto
+void logError(T...)(string fmt, auto ref T args) nothrow { log(LogLevel.Error, fmt, args); }
+
+/// ditto
+void log(T...)(LogLevel level, string fmt, auto ref T args)
+nothrow {
+	if( level < s_minLevel ) return;
+	string pref;
+	final switch( level ){
+		case LogLevel.Trace: pref = "trc"; break;
+		case LogLevel.Debug: pref = "dbg"; break;
+		case LogLevel.Info: pref = "INF"; break;
+		case LogLevel.Warn: pref = "WRN"; break;
+		case LogLevel.Error: pref = "ERR"; break;
+		case LogLevel.Fatal: pref = "FATAL"; break;
+		case LogLevel.None: assert(false);
+	}
+
+	try {
+		auto txt = appender!string();
+		txt.reserve(256);
+		formattedWrite(txt, fmt, args);
+
+		auto threadid = cast(ulong)cast(void*)Thread.getThis();
+		auto fiberid = cast(ulong)cast(void*)Fiber.getThis();
+		threadid ^= threadid >> 32;
+		fiberid ^= fiberid >> 32;
+
+		if( level >= s_minLevel ){
+			if( s_plainLogging ) writeln(txt.data());
+			else writefln("[%08X:%08X %s] %s", threadid, fiberid, pref, txt.data());
+			stdout.flush();
+		}
+	} catch( Exception e ){
+		// this is bad but what can we do..
+		debug assert(false, e.msg);
+	}
+}
+
+/// Specifies the log level for a particular log message.
+enum LogLevel {
+	Trace,
+	Debug,
+	Info,
+	Warn,
+	Error,
+	Fatal,
+	None
+}
+
diff --git a/source/vibe/data/json.d b/source/vibe/data/json.d
new file mode 100644
index 0000000..0d6169b
--- /dev/null
+++ b/source/vibe/data/json.d
@@ -0,0 +1,1282 @@
+/**
+	JSON serialization and value handling.
+
+	This module provides the Json struct for reading, writing and manipulating JSON values in a seamless,
+	JavaScript like way. De(serialization) of arbitrary D types is also supported.
+
+	Examples:
+
+	---
+	void manipulateJson(Json j)
+	{
+		// object members can be accessed using member syntax, just like in JavaScript
+		j = Json.EmptyObject;
+		j.name = "Example";
+		j.id = 1;
+
+		// retrieving the values is done using get()
+		assert(j["name"].get!string == "Example");
+		assert(j["id"].get!int == 1);
+
+		// semantic convertions can be done using to()
+		assert(j.id.to!string == "1");
+
+		// prints:
+		// name: "Example"
+		// id: 1
+		foreach( string key, value; j ){
+			writefln("%s: %s", key, value);
+		}
+
+		// print out as JSON: {"name": "Example", "id": 1}
+		writefln("JSON: %s", j.toString());
+	}
+	---
+
+	Copyright: © 2012 RejectedSoftware e.K.
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Sönke Ludwig
+*/
+module vibe.data.json;
+
+import vibe.data.utils;
+
+import std.array;
+import std.conv;
+import std.datetime;
+import std.exception;
+import std.format;
+import std.string;
+import std.range;
+import std.traits;
+
+
+/******************************************************************************/
+/* public types                                                               */
+/******************************************************************************/
+
+/**
+	Represents a single JSON value.
+
+	Json values can have one of the types defined in the Json.Type enum. They
+	behave mostly like values in ECMA script in the way that you can
+	transparently perform operations on them. However, strict typechecking is
+	done, so that operations between differently typed JSON values will throw
+	an exception. Additionally, an explicit cast or using get!() or to!() is
+	required to convert a JSON value to the corresponding static D type.
+*/
+struct Json {
+	private {
+		union {
+			bool m_bool;
+			long m_int;
+			double m_float;
+			string m_string;
+			Json[] m_array;
+			Json[string] m_object;
+		};
+		Type m_type = Type.Undefined;
+	}
+
+	/** Represents the run time type of a JSON value.
+	*/
+	enum Type {
+		/// A non-existent value in a JSON object
+		Undefined,
+		/// Null value
+		Null,
+		/// Boolean value
+		Bool,
+		/// 64-bit integer value
+		Int,
+		/// 64-bit floating point value
+		Float,
+		/// UTF-8 string
+		String,
+		/// Array of JSON values
+		Array,
+		/// JSON object aka. dictionary from string to Json
+		Object
+	}
+
+	/// New JSON value of Type.Undefined
+	static @property Json Undefined() { return Json(); }
+
+	/// New JSON value of Type.Object
+	static @property Json EmptyObject() { return Json(cast(Json[string])null); }
+
+	/// New JSON value of Type.Array
+	static @property Json EmptyArray() { return Json(cast(Json[])null); }
+
+	version(JsonLineNumbers) int line;
+
+	/**
+		Constructor for a JSON object.
+	*/
+	this(typeof(null)) { m_type = Type.Null; }
+	/// ditto
+	this(bool v) { m_type = Type.Bool; m_bool = v; }
+	/// ditto
+	this(int v) { m_type = Type.Int; m_int = v; }
+	/// ditto
+	this(long v) { m_type = Type.Int; m_int = v; }
+	/// ditto
+	this(double v) { m_type = Type.Float; m_float = v; }
+	/// ditto
+	this(string v) { m_type = Type.String; m_string = v; }
+	/// ditto
+	this(Json[] v) { m_type = Type.Array; m_array = v; }
+	/// ditto
+	this(Json[string] v) { m_type = Type.Object; m_object = v; }
+
+	/**
+		Allows assignment of D values to a JSON value.
+	*/
+	ref Json opAssign(Json v){
+		m_type = v.m_type;
+		final switch(m_type){
+			case Type.Undefined: m_string = null; break;
+			case Type.Null: m_string = null; break;
+			case Type.Bool: m_bool = v.m_bool; break;
+			case Type.Int: m_int = v.m_int; break;
+			case Type.Float: m_float = v.m_float; break;
+			case Type.String: m_string = v.m_string; break;
+			case Type.Array: m_array = v.m_array; break;
+			case Type.Object: m_object = v.m_object; break;
+		}
+		return this;
+	}
+	/// ditto
+	void opAssign(typeof(null)) { m_type = Type.Null; m_string = null; }
+	/// ditto
+	bool opAssign(bool v) { m_type = Type.Bool; m_bool = v; return v; }
+	/// ditto
+	int opAssign(int v) { m_type = Type.Int; m_int = v; return v; }
+	/// ditto
+	long opAssign(long v) { m_type = Type.Int; m_int = v; return v; }
+	/// ditto
+	double opAssign(double v) { m_type = Type.Float; m_float = v; return v; }
+	/// ditto
+	string opAssign(string v) { m_type = Type.String; m_string = v; return v; }
+	/// ditto
+	Json[] opAssign(Json[] v) { m_type = Type.Array; m_array = v; return v; }
+	/// ditto
+	Json[string] opAssign(Json[string] v) { m_type = Type.Object; m_object = v; return v; }
+
+	/**
+		The current type id of this JSON object.
+	*/
+	@property Type type() const { return m_type; }
+
+	/**
+		Allows direct indexing of array typed JSON values.
+	*/
+	ref inout(Json) opIndex(size_t idx) inout { checkType!(Json[])(); return m_array[idx]; }
+
+	/**
+		Allows direct indexing of object typed JSON values using a string as
+		the key.
+	*/
+	const(Json) opIndex(string key) const {
+		checkType!(Json[string])();
+		if( auto pv = key in m_object ) return *pv;
+		Json ret = Json.Undefined;
+		ret.m_string = key;
+		return ret;
+	}
+	/// ditto
+	ref Json opIndex(string key){
+		checkType!(Json[string])();
+		if( auto pv = key in m_object )
+			return *pv;
+		m_object[key] = Json();
+		m_object[key].m_type = Type.Undefined; // DMDBUG: AAs are teh $H1T!!!11
+		assert(m_object[key].type == Type.Undefined);
+		m_object[key].m_string = key;
+		return m_object[key];
+	}
+
+	/**
+		Returns a slice of a JSON array.
+	*/
+	inout(Json[]) opSlice() inout { checkType!(Json[])(); return m_array; }
+	///
+	inout(Json[]) opSlice(size_t from, size_t to) inout { checkType!(Json[])(); return m_array[from .. to]; }
+
+	/**
+		Returns the number of entries of string, array or object typed JSON values.
+	*/
+	@property size_t length()
+	const {
+		switch(m_type){
+			case Type.String: return m_string.length;
+			case Type.Array: return m_array.length;
+			case Type.Object: return m_object.length;
+			default:
+				enforce(false, "Json.length() can only be called on strings, arrays and objects, not "~.to!string(m_type)~".");
+				return 0;
+		}
+	}
+
+	/**
+		Allows foreach iterating over JSON objects and arrays.
+	*/
+	int opApply(int delegate(ref Json obj) del)
+	{
+		enforce(m_type == Type.Array || m_type == Type.Object, "opApply may only be called on objects and arrays, not "~.to!string(m_type)~".");
+		if( m_type == Type.Array ){
+			foreach( ref v; m_array )
+				if( auto ret = del(v) )
+					return ret;
+			return 0;
+		} else {
+			foreach( ref v; m_object )
+				if( v.type != Type.Undefined )
+					if( auto ret = del(v) )
+						return ret;
+			return 0;
+		}
+	}
+	/// ditto
+	int opApply(int delegate(ref const Json obj) del)
+	const {
+		enforce(m_type == Type.Array || m_type == Type.Object, "opApply may only be called on objects and arrays, not "~.to!string(m_type)~".");
+		if( m_type == Type.Array ){
+			foreach( ref v; m_array )
+				if( auto ret = del(v) )
+					return ret;
+			return 0;
+		} else {
+			foreach( ref v; m_object )
+				if( v.type != Type.Undefined )
+					if( auto ret = del(v) )
+						return ret;
+			return 0;
+		}
+	}
+	/// ditto
+	int opApply(int delegate(ref size_t idx, ref Json obj) del)
+	{
+		enforce(m_type == Type.Array, "opApply may only be called on arrays, not "~.to!string(m_type)~"");
+		foreach( idx, ref v; m_array )
+			if( auto ret = del(idx, v) )
+				return ret;
+		return 0;
+	}
+	/// ditto
+	int opApply(int delegate(ref size_t idx, ref const Json obj) del)
+	const {
+		enforce(m_type == Type.Array, "opApply may only be called on arrays, not "~.to!string(m_type)~".");
+		foreach( idx, ref v; m_array )
+			if( auto ret = del(idx, v) )
+				return ret;
+		return 0;
+	}
+	/// ditto
+	int opApply(int delegate(ref string idx, ref Json obj) del)
+	{
+		enforce(m_type == Type.Object, "opApply may only be called on objects, not "~.to!string(m_type)~".");
+		foreach( idx, ref v; m_object )
+			if( v.type != Type.Undefined )
+				if( auto ret = del(idx, v) )
+					return ret;
+		return 0;
+	}
+	/// ditto
+	int opApply(int delegate(ref string idx, ref const Json obj) del)
+	const {
+		enforce(m_type == Type.Object, "opApply may only be called on objects, not "~.to!string(m_type)~".");
+		foreach( idx, ref v; m_object )
+			if( v.type != Type.Undefined )
+				if( auto ret = del(idx, v) )
+					return ret;
+		return 0;
+	}
+
+	/**
+		Converts the JSON value to the corresponding D type - types must match exactly.
+	*/
+	inout(T) opCast(T)() inout { return get!T; }
+	/// ditto
+	@property inout(T) get(T)()
+	inout {
+		checkType!T();
+		static if( is(T == bool) ) return m_bool;
+		else static if( is(T == double) ) return m_float;
+		else static if( is(T == float) ) return cast(T)m_float;
+		else static if( is(T == long) ) return m_int;
+		else static if( is(T : long) ){ enforce(m_int <= T.max && m_int >= T.min); return cast(T)m_int; }
+		else static if( is(T == string) ) return m_string;
+		else static if( is(T == Json[]) ) return m_array;
+		else static if( is(T == Json[string]) ) return m_object;
+		else static assert("JSON can only be casted to (bool, long, double, string, Json[] or Json[string]. Not "~T.stringof~".");
+	}
+	/// ditto
+	@property const(T) opt(T)(const(T) def = T.init)
+	const {
+		if( typeId!T != m_type ) return def;
+		return get!T;
+	}
+	/// ditto
+	@property T opt(T)(T def = T.init)
+	{
+		if( typeId!T != m_type ) return def;
+		return get!T;
+	}
+
+	/**
+		Converts the JSON value to the corresponding D type - types are converted as neccessary.
+	*/
+	@property inout(T) to(T)()
+	inout {
+		static if( is(T == bool) ){
+			final switch( m_type ){
+				case Type.Undefined: return false;
+				case Type.Null: return false;
+				case Type.Bool: return m_bool;
+				case Type.Int: return m_int != 0;
+				case Type.Float: return m_float != 0;
+				case Type.String: return m_string.length > 0;
+				case Type.Array: return m_array.length > 0;
+				case Type.Object: return m_object.length > 0;
+			}
+		} else static if( is(T == double) ){
+			final switch( m_type ){
+				case Type.Undefined: return T.init;
+				case Type.Null: return 0;
+				case Type.Bool: return m_bool ? 1 : 0;
+				case Type.Int: return m_int;
+				case Type.Float: return m_float;
+				case Type.String: return .to!double(cast(string)m_string);
+				case Type.Array: return double.init;
+				case Type.Object: return double.init;
+			}
+		} else static if( is(T == float) ){
+			final switch( m_type ){
+				case Type.Undefined: return T.init;
+				case Type.Null: return 0;
+				case Type.Bool: return m_bool ? 1 : 0;
+				case Type.Int: return m_int;
+				case Type.Float: return m_float;
+				case Type.String: return .to!float(cast(string)m_string);
+				case Type.Array: return float.init;
+				case Type.Object: return float.init;
+			}
+		}
+		else static if( is(T == long) ){
+			final switch( m_type ){
+				case Type.Undefined: return 0;
+				case Type.Null: return 0;
+				case Type.Bool: return m_bool ? 1 : 0;
+				case Type.Int: return m_int;
+				case Type.Float: return cast(long)m_float;
+				case Type.String: return .to!long(m_string);
+				case Type.Array: return 0;
+				case Type.Object: return 0;
+			}
+		} else static if( is(T : long) ){
+			final switch( m_type ){
+				case Type.Undefined: return 0;
+				case Type.Null: return 0;
+				case Type.Bool: return m_bool ? 1 : 0;
+				case Type.Int: return cast(T)m_int;
+				case Type.Float: return cast(T)m_float;
+				case Type.String: return cast(T).to!long(cast(string)m_string);
+				case Type.Array: return 0;
+				case Type.Object: return 0;
+			}
+		} else static if( is(T == string) ){
+			switch( m_type ){
+				default: return toString();
+				case Type.String: return m_string;
+			}
+		} else static if( is(T == Json[]) ){
+			switch( m_type ){
+				default: return Json([this]);
+				case Type.Array: return m_array;
+			}
+		} else static if( is(T == Json[string]) ){
+			switch( m_type ){
+				default: return Json(["value": this]);
+				case Type.Object: return m_object;
+			}
+		} else static assert("JSON can only be casted to (bool, long, double, string, Json[] or Json[string]. Not "~T.stringof~".");
+	}
+
+	/**
+		Performs unary operations on the JSON value.
+
+		The following operations are supported for each type:
+
+		$(DL
+			$(DT Null)   $(DD none)
+			$(DT Bool)   $(DD ~)
+			$(DT Int)    $(DD +, -, ++, --)
+			$(DT Float)  $(DD +, -, ++, --)
+			$(DT String) $(DD none)
+			$(DT Array)  $(DD none)
+			$(DT Object) $(DD none)
+		)
+	*/
+	Json opUnary(string op)()
+	const {
+		static if( op == "~" ){
+			checkType!bool();
+			return Json(~m_bool);
+		} else static if( op == "+" || op == "-" || op == "++" || op == "--" ){
+			if( m_type == Type.Int ) mixin("return Json("~op~"m_int);");
+			else if( m_type == Type.Float ) mixin("return Json("~op~"m_float);");
+			else enforce(false, "'"~op~"' only allowed on scalar types, not on "~.to!string(m_type)~".");
+		} else static assert("Unsupported operator '"~op~"' for type JSON.");
+	}
+
+	/**
+		Performs binary operations between JSON values.
+
+		The two JSON values must be of the same run time type or an exception
+		will be thrown. Only the operations listed are allowed for each of the
+		types.
+
+		$(DL
+			$(DT Null)   $(DD none)
+			$(DT Bool)   $(DD &&, ||)
+			$(DT Int)    $(DD +, -, *, /, %)
+			$(DT Float)  $(DD +, -, *, /, %)
+			$(DT String) $(DD ~)
+			$(DT Array)  $(DD ~)
+			$(DT Object) $(DD none)
+		)
+	*/
+	Json opBinary(string op)(ref const(Json) other)
+	const {
+		enforce(m_type == other.m_type, "Binary operation '"~op~"' between "~.to!string(m_type)~" and "~.to!string(other.m_type)~" JSON objects.");
+		static if( op == "&&" ){
+			enforce(m_type == Type.Bool, "'&&' only allowed for Type.Bool, not "~.to!string(m_type)~".");
+			return Json(m_bool && other.m_bool);
+		} else static if( op == "||" ){
+			enforce(m_type == Type.Bool, "'||' only allowed for Type.Bool, not "~.to!string(m_type)~".");
+			return Json(m_bool || other.m_bool);
+		} else static if( op == "+" ){
+			if( m_type == Type.Int ) return Json(m_int + other.m_int);
+			else if( m_type == Type.Float ) return Json(m_float + other.m_float);
+			else enforce(false, "'+' only allowed for scalar types, not "~.to!string(m_type)~".");
+		} else static if( op == "-" ){
+			if( m_type == Type.Int ) return Json(m_int - other.m_int);
+			else if( m_type == Type.Float ) return Json(m_float - other.m_float);
+			else enforce(false, "'-' only allowed for scalar types, not "~.to!string(m_type)~".");
+		} else static if( op == "*" ){
+			if( m_type == Type.Int ) return Json(m_int * other.m_int);
+			else if( m_type == Type.Float ) return Json(m_float * other.m_float);
+			else enforce(false, "'*' only allowed for scalar types, not "~.to!string(m_type)~".");
+		} else static if( op == "/" ){
+			if( m_type == Type.Int ) return Json(m_int / other.m_int);
+			else if( m_type == Type.Float ) return Json(m_float / other.m_float);
+			else enforce(false, "'/' only allowed for scalar types, not "~.to!string(m_type)~".");
+		} else static if( op == "%" ){
+			if( m_type == Type.Int ) return Json(m_int % other.m_int);
+			else if( m_type == Type.Float ) return Json(m_float % other.m_float);
+			else enforce(false, "'%' only allowed for scalar types, not "~.to!string(m_type)~".");
+		} else static if( op == "~" ){
+			if( m_type == Type.String ) return Json(m_string ~ other.m_string);
+			else enforce(false, "'~' only allowed for strings, not "~.to!string(m_type)~".");
+		} else static assert("Unsupported operator '"~op~"' for type JSON.");
+		assert(false);
+	}
+	/// ditto
+	Json opBinary(string op)(Json other)
+		if( op == "~" )
+	{
+		static if( op == "~" ){
+			if( m_type == Type.String ) return Json(m_string ~ other.m_string);
+			else if( m_type == Type.Array ) return Json(m_array ~ other.m_array);
+			else enforce(false, "'~' only allowed for strings and arrays, not "~.to!string(m_type)~".");
+		} else static assert("Unsupported operator '"~op~"' for type JSON.");
+		assert(false);
+	}
+	/// ditto
+	void opOpAssign(string op)(Json other)
+		if( op == "+" || op == "-" || op == "*" ||op == "/" || op == "%" )
+	{
+		enforce(m_type == other.m_type, "Binary operation '"~op~"' between "~.to!string(m_type)~" and "~.to!string(other.m_type)~" JSON objects.");
+		static if( op == "+" ){
+			if( m_type == Type.Int ) m_int += other.m_int;
+			else if( m_type == Type.Float ) m_float += other.m_float;
+			else enforce(false, "'+' only allowed for scalar types, not "~.to!string(m_type)~".");
+		} else static if( op == "-" ){
+			if( m_type == Type.Int ) m_int -= other.m_int;
+			else if( m_type == Type.Float ) m_float -= other.m_float;
+			else enforce(false, "'-' only allowed for scalar types, not "~.to!string(m_type)~".");
+		} else static if( op == "*" ){
+			if( m_type == Type.Int ) m_int *= other.m_int;
+			else if( m_type == Type.Float ) m_float *= other.m_float;
+			else enforce(false, "'*' only allowed for scalar types, not "~.to!string(m_type)~".");
+		} else static if( op == "/" ){
+			if( m_type == Type.Int ) m_int /= other.m_int;
+			else if( m_type == Type.Float ) m_float /= other.m_float;
+			else enforce(false, "'/' only allowed for scalar types, not "~.to!string(m_type)~".");
+		} else static if( op == "%" ){
+			if( m_type == Type.Int ) m_int %= other.m_int;
+			else if( m_type == Type.Float ) m_float %= other.m_float;
+			else enforce(false, "'%' only allowed for scalar types, not "~.to!string(m_type)~".");
+		} /*else static if( op == "~" ){
+			if( m_type == Type.String ) m_string ~= other.m_string;
+			else if( m_type == Type.Array ) m_array ~= other.m_array;
+			else enforce(false, "'%' only allowed for scalar types, not "~.to!string(m_type)~".");
+		}*/ else static assert("Unsupported operator '"~op~"' for type JSON.");
+		assert(false);
+	}
+	/// ditto
+	Json opBinary(string op)(bool other) const { checkType!bool(); mixin("return Json(m_bool "~op~" other);"); }
+	/// ditto
+	Json opBinary(string op)(long other) const { checkType!long(); mixin("return Json(m_int "~op~" other);"); }
+	/// ditto
+	Json opBinary(string op)(double other) const { checkType!double(); mixin("return Json(m_float "~op~" other);"); }
+	/// ditto
+	Json opBinary(string op)(string other) const { checkType!string(); mixin("return Json(m_string "~op~" other);"); }
+	/// ditto
+	Json opBinary(string op)(Json[] other) { checkType!(Json[])(); mixin("return Json(m_array "~op~" other);"); }
+	/// ditto
+	Json opBinaryRight(string op)(bool other) const { checkType!bool(); mixin("return Json(other "~op~" m_bool);"); }
+	/// ditto
+	Json opBinaryRight(string op)(long other) const { checkType!long(); mixin("return Json(other "~op~" m_int);"); }
+	/// ditto
+	Json opBinaryRight(string op)(double other) const { checkType!double(); mixin("return Json(other "~op~" m_float);"); }
+	/// ditto
+	Json opBinaryRight(string op)(string other) const if(op == "~") { checkType!string(); return Json(other ~ m_string); }
+	/// ditto
+	inout(Json)* opBinaryRight(string op)(string other) inout if(op == "in") {
+		checkType!(Json[string])();
+		auto pv = other in m_object;
+		if( !pv ) return null;
+		if( pv.type == Type.Undefined ) return null;
+		return pv;
+	}
+	/// ditto
+	Json opBinaryRight(string op)(Json[] other) { checkType!(Json[])(); mixin("return Json(other "~op~" m_array);"); }
+
+	/**
+		Allows to access existing fields of a JSON object using dot syntax.
+	*/
+	@property const(Json) opDispatch(string prop)() const { return opIndex(prop); }
+	/// ditto
+	@property ref Json opDispatch(string prop)() { return opIndex(prop); }
+
+	/**
+		Compares two JSON values for equality.
+
+		If the two values have different types, they are considered unequal.
+		This differs with ECMA script, which performs a type conversion before
+		comparing the values.
+	*/
+	bool opEquals(ref const Json other)
+	const {
+		if( m_type != other.m_type ) return false;
+		final switch(m_type){
+			case Type.Undefined: return false;
+			case Type.Null: return true;
+			case Type.Bool: return m_bool == other.m_bool;
+			case Type.Int: return m_int == other.m_int;
+			case Type.Float: return m_float == other.m_float;
+			case Type.String: return m_string == other.m_string;
+			case Type.Array: return m_array == other.m_array;
+			case Type.Object: return m_object == other.m_object;
+		}
+	}
+	/// ditto
+	bool opEquals(const Json other) const { return opEquals(other); }
+	/// ditto
+	bool opEquals(typeof(null)) const { return m_type == Type.Null; }
+	/// ditto
+	bool opEquals(bool v) const { return m_type == Type.Bool && m_bool == v; }
+	/// ditto
+	bool opEquals(long v) const { return m_type == Type.Int && m_int == v; }
+	/// ditto
+	bool opEquals(double v) const { return m_type == Type.Float && m_float == v; }
+	/// ditto
+	bool opEquals(string v) const { return m_type == Type.String && m_string == v; }
+
+	/**
+		Compares two JSON values.
+
+		If the types of the two values differ, the value with the smaller type
+		id is considered the smaller value. This differs from ECMA script, which
+		performs a type conversion before comparing the values.
+
+		JSON values of type Object cannot be compared and will throw an
+		exception.
+	*/
+	int opCmp(ref const Json other)
+	const {
+		if( m_type != other.m_type ) return m_type < other.m_type ? -1 : 1;
+		final switch(m_type){
+			case Type.Undefined: return 0;
+			case Type.Null: return 0;
+			case Type.Bool: return m_bool < other.m_bool ? -1 : m_bool == other.m_bool ? 0 : 1;
+			case Type.Int: return m_int < other.m_int ? -1 : m_int == other.m_int ? 0 : 1;
+			case Type.Float: return m_float < other.m_float ? -1 : m_float == other.m_float ? 0 : 1;
+			case Type.String: return m_string < other.m_string ? -1 : m_string == other.m_string ? 0 : 1;
+			case Type.Array: return m_array < other.m_array ? -1 : m_array == other.m_array ? 0 : 1;
+			case Type.Object:
+				enforce(false, "JSON objects cannot be compared.");
+				assert(false);
+		}
+	}
+
+
+
+	/**
+		Returns the type id corresponding to the given D type.
+	*/
+	static @property Type typeId(T)() {
+		static if( is(T == typeof(null)) ) return Type.Null;
+		else static if( is(T == bool) ) return Type.Bool;
+		else static if( is(T == double) ) return Type.Float;
+		else static if( is(T == float) ) return Type.Float;
+		else static if( is(T : long) ) return Type.Int;
+		else static if( is(T == string) ) return Type.String;
+		else static if( is(T == Json[]) ) return Type.Array;
+		else static if( is(T == Json[string]) ) return Type.Object;
+		else static assert(false, "Unsupported JSON type '"~T.stringof~"'. Only bool, long, double, string, Json[] and Json[string] are allowed.");
+	}
+
+	/**
+		Returns the JSON object as a string.
+
+		For large JSON values use writeJsonString instead as this function will store the whole string
+		in memory, whereas writeJsonString writes it out bit for bit.
+
+		See_Also: writeJsonString, toPrettyString
+	*/
+	string toString()
+	const {
+		auto ret = appender!string();
+		writeJsonString(ret, this);
+		return ret.data;
+	}
+
+	/**
+		Returns the JSON object as a "pretty" string.
+
+		---
+		auto json = Json(["foo": Json("bar")]);
+		writeln(json.toPrettyString());
+
+		// output:
+		// {
+		//     "foo": "bar"
+		// }
+		---
+
+		Params:
+			level = Specifies the base amount of indentation for the output. Indentation  is always
+				done using tab characters.
+
+		See_Also: writePrettyJsonString, toString
+	*/
+	string toPrettyString(int level = 0)
+	const {
+		auto ret = appender!string();
+		writePrettyJsonString(ret, this, level);
+		return ret.data;
+	}
+
+	private void checkType(T)()
+	const {
+		string dbg;
+		if( m_type == Type.Undefined ) dbg = " field "~m_string;
+		enforce(typeId!T == m_type, "Trying to access JSON"~dbg~" of type "~.to!string(m_type)~" as "~T.stringof~".");
+	}
+
+	/*invariant()
+	{
+		assert(m_type >= Type.Undefined && m_type <= Type.Object);
+	}*/
+}
+
+
+/******************************************************************************/
+/* public functions                                                           */
+/******************************************************************************/
+
+/**
+	Parses the given range as a JSON string and returns the corresponding Json object.
+
+	The range is shrunk during parsing, leaving any remaining text that is not part of
+	the JSON contents.
+
+	Throws an Exception if any parsing error occured.
+*/
+Json parseJson(R)(ref R range, int* line = null)
+	if( is(R == string) )
+{
+	Json ret;
+	enforce(!range.empty, "JSON string is empty.");
+
+	skipWhitespace(range, line);
+
+	version(JsonLineNumbers){
+		import vibe.core.log;
+		int curline = line ? *line : 0;
+		scope(failure) logError("Error in line: %d", curline);
+	}
+
+	switch( range.front ){
+		case 'f':
+			enforce(range[1 .. $].startsWith("alse"), "Expected 'false', got '"~range[0 .. 5]~"'.");
+			range.popFrontN(5);
+			ret = false;
+			break;
+		case 'n':
+			enforce(range[1 .. $].startsWith("ull"), "Expected 'null', got '"~range[0 .. 4]~"'.");
+			range.popFrontN(4);
+			ret = null;
+			break;
+		case 't':
+			enforce(range[1 .. $].startsWith("rue"), "Expected 'true', got '"~range[0 .. 4]~"'.");
+			range.popFrontN(4);
+			ret = true;
+			break;
+		case '0': .. case '9'+1:
+		case '-':
+			bool is_float;
+			auto num = skipNumber(range, is_float);
+			if( is_float ) ret = to!double(num);
+			else ret = to!long(num);
+			break;
+		case '\"':
+			ret = skipJsonString(range);
+			break;
+		case '[':
+			Json[] arr;
+			range.popFront();
+			while(true) {
+				skipWhitespace(range, line);
+				enforce(!range.empty);
+				if(range.front == ']') break;
+				arr ~= parseJson(range, line);
+				skipWhitespace(range, line);
+				enforce(!range.empty && (range.front == ',' || range.front == ']'), "Expected ']' or ','.");
+				if( range.front == ']' ) break;
+				else range.popFront();
+			}
+			range.popFront();
+			ret = arr;
+			break;
+		case '{':
+			Json[string] obj;
+			range.popFront();
+			while(true) {
+				skipWhitespace(range, line);
+				enforce(!range.empty);
+				if(range.front == '}') break;
+				string key = skipJsonString(range);
+				skipWhitespace(range, line);
+				enforce(range.startsWith(":"), "Expected ':' for key '" ~ key ~ "'");
+				range.popFront();
+				skipWhitespace(range, line);
+				Json itm = parseJson(range, line);
+				obj[key] = itm;
+				skipWhitespace(range, line);
+				enforce(!range.empty && (range.front == ',' || range.front == '}'), "Expected '}' or ',' - got '"~range[0]~"'.");
+				if( range.front == '}' ) break;
+				else range.popFront();
+			}
+			range.popFront();
+			ret = obj;
+			break;
+		default:
+			enforce(false, "Expected valid json token, got '"~to!string(range.length)~range[0 .. range.length>12?12:range.length]~"'.");
+	}
+
+	assert(ret.type != Json.Type.Undefined);
+	version(JsonLineNumbers) ret.line = curline;
+	return ret;
+}
+
+/**
+	Parses the given JSON string and returns the corresponding Json object.
+
+	Throws an Exception if any parsing error occurs.
+*/
+Json parseJsonString(string str)
+{
+	auto ret = parseJson(str);
+	enforce(str.strip().length == 0, "Expected end of string after JSON value.");
+	return ret;
+}
+
+unittest {
+	assert(parseJsonString("null") == Json(null));
+	assert(parseJsonString("true") == Json(true));
+	assert(parseJsonString("false") == Json(false));
+	assert(parseJsonString("1") == Json(1));
+	assert(parseJsonString("2.0") == Json(2.0));
+	assert(parseJsonString("\"test\"") == Json("test"));
+	assert(parseJsonString("[1, 2, 3]") == Json([Json(1), Json(2), Json(3)]));
+	assert(parseJsonString("{\"a\": 1}") == Json(["a": Json(1)]));
+	assert(parseJsonString(`"\\\/\b\f\n\r\t\u1234"`).get!string == "\\/\b\f\n\r\t\u1234");
+}
+
+
+/**
+	Serializes the given value to JSON.
+
+	The following types of values are supported:
+
+	$(DL
+		$(DT Json)            $(DD Used as-is)
+		$(DT null)            $(DD Converted to Json.Type.Null)
+		$(DT bool)            $(DD Converted to Json.Type.Bool)
+		$(DT float, double)   $(DD Converted to Json.Type.Double)
+		$(DT short, ushort, int, uint, long, ulong) $(DD Converted to Json.Type.Int)
+		$(DT string)          $(DD Converted to Json.Type.String)
+		$(DT T[])             $(DD Converted to Json.Type.Array)
+		$(DT T[string])       $(DD Converted to Json.Type.Object)
+		$(DT struct)          $(DD Converted to Json.Type.Object)
+		$(DT class)           $(DD Converted to Json.Type.Object or Json.Type.Null)
+	)
+
+	All entries of an array or an associative array, as well as all R/W properties and
+	all public fields of a struct/class are recursively serialized using the same rules.
+
+	Fields ending with an underscore will have the last underscore stripped in the
+	serialized output. This makes it possible to use fields with D keywords as their name
+	by simply appending an underscore.
+
+	The following methods can be used to customize the serialization of structs/classes:
+
+	---
+	Json toJson() const;
+	static T fromJson(Json src);
+
+	string toString() const;
+	static T fromString(string src);
+	---
+
+	The methods will have to be defined in pairs. The first pair that is implemented by
+	the type will be used for serialization (i.e. toJson overrides toString).
+*/
+Json serializeToJson(T)(T value)
+{
+	alias Unqual!T TU;
+	static if( is(TU == Json) ) return value;
+	else static if( is(TU == typeof(null)) ) return Json(null);
+	else static if( is(TU == bool) ) return Json(value);
+	else static if( is(TU == float) ) return Json(cast(double)value);
+	else static if( is(TU == double) ) return Json(value);
+	else static if( is(TU == DateTime) ) return Json(value.toISOExtString());
+	else static if( is(TU == SysTime) ) return Json(value.toISOExtString());
+	else static if( is(TU : long) ) return Json(cast(long)value);
+	else static if( is(TU == string) ) return Json(value);
+	else static if( isArray!T ){
+		auto ret = new Json[value.length];
+		foreach( i; 0 .. value.length )
+			ret[i] = serializeToJson(value[i]);
+		return Json(ret);
+	} else static if( isAssociativeArray!TU ){
+		Json[string] ret;
+		foreach( string key, value; value )
+			ret[key] = serializeToJson(value);
+		return Json(ret);
+	} else static if( __traits(compiles, value = T.fromJson(value.toJson())) ){
+		return value.toJson();
+	} else static if( __traits(compiles, value = T.fromString(value.toString())) ){
+		return Json(value.toString());
+	} else static if( is(TU == struct) ){
+		Json[string] ret;
+		foreach( m; __traits(allMembers, T) ){
+			static if( isRWField!(TU, m) ){
+				auto mv = __traits(getMember, value, m);
+				ret[underscoreStrip(m)] = serializeToJson(mv);
+			}
+		}
+		return Json(ret);
+	} else static if( is(TU == class) ){
+		if( value is null ) return Json(null);
+		Json[string] ret;
+		foreach( m; __traits(allMembers, T) ){
+			static if( isRWField!(TU, m) ){
+				auto mv = __traits(getMember, value, m);
+				ret[underscoreStrip(m)] = serializeToJson(mv);
+			}
+		}
+		return Json(ret);
+	} else static if( isPointer!TU ){
+		if( value is null ) return Json(null);
+		return serializeToJson(*value);
+	} else {
+		static assert(false, "Unsupported type '"~T.stringof~"' for JSON serialization.");
+	}
+}
+
+
+/**
+	Deserializes a JSON value into the destination variable.
+
+	The same types as for serializeToJson() are supported and handled inversely.
+*/
+void deserializeJson(T)(ref T dst, Json src)
+{
+	dst = deserializeJson!T(src);
+}
+/// ditto
+T deserializeJson(T)(Json src)
+{
+	static if( is(T == Json) ) return src;
+	else static if( is(T == typeof(null)) ){ return null; }
+	else static if( is(T == bool) ) return src.get!bool;
+	else static if( is(T == float) ) return src.to!float;   // since doubles are frequently serialized without
+	else static if( is(T == double) ) return src.to!double; // a decimal point, we allow conversions here
+	else static if( is(T == DateTime) ) return DateTime.fromISOExtString(src.get!string);
+	else static if( is(T == SysTime) ) return SysTime.fromISOExtString(src.get!string);
+	else static if( is(T : long) ) return cast(T)src.get!long;
+	else static if( is(T == string) ) return src.get!string;
+	else static if( isArray!T ){
+		alias typeof(T.init[0]) TV;
+		auto dst = new Unqual!TV[src.length];
+		foreach( size_t i, v; src )
+			dst[i] = deserializeJson!(Unqual!TV)(v);
+		return dst;
+	} else static if( isAssociativeArray!T ){
+		alias typeof(T.init.values[0]) TV;
+		Unqual!TV[string] dst;
+		foreach( string key, value; src )
+			dst[key] = deserializeJson!(Unqual!TV)(value);
+		return dst;
+	} else static if( __traits(compiles, { T dst; dst = T.fromJson(dst.toJson()); }()) ){
+		return T.fromJson(src);
+	} else static if( __traits(compiles, { T dst; dst = T.fromString(dst.toString()); }()) ){
+		return T.fromString(src.get!string);
+	} else static if( is(T == struct) ){
+		T dst;
+		foreach( m; __traits(allMembers, T) ){
+			static if( isRWPlainField!(T, m) || isRWField!(T, m) ){
+				alias typeof(__traits(getMember, dst, m)) TM;
+				__traits(getMember, dst, m) = deserializeJson!TM(src[underscoreStrip(m)]);
+			}
+		}
+		return dst;
+	} else static if( is(T == class) ){
+		if( src.type == Json.Type.Null ) return null;
+		auto dst = new T;
+		foreach( m; __traits(allMembers, T) ){
+			static if( isRWPlainField!(T, m) || isRWField!(T, m) ){
+				alias typeof(__traits(getMember, dst, m)) TM;
+				__traits(getMember, dst, m) = deserializeJson!TM(src[underscoreStrip(m)]);
+			}
+		}
+		return dst;
+	} else static if( isPointer!T ){
+		if( src.type == Json.Type.Null ) return null;
+		alias typeof(*T.init) TD;
+		dst = new TD;
+		*dst = deserializeJson!TD(src);
+		return dst;
+	} else {
+		static assert(false, "Unsupported type '"~T.stringof~"' for JSON serialization.");
+	}
+}
+
+unittest {
+	import std.stdio;
+	static struct S { float a; double b; bool c; int d; string e; byte f; ubyte g; long h; ulong i; float[] j; }
+	immutable S t = {1.5, -3.0, true, int.min, "Test", -128, 255, long.min, ulong.max, [1.1, 1.2, 1.3]};
+	S u;
+	deserializeJson(u, serializeToJson(t));
+	assert(t.a == u.a);
+	assert(t.b == u.b);
+	assert(t.c == u.c);
+	assert(t.d == u.d);
+	assert(t.e == u.e);
+	assert(t.f == u.f);
+	assert(t.g == u.g);
+	assert(t.h == u.h);
+	assert(t.i == u.i);
+	assert(t.j == u.j);
+}
+
+unittest {
+	static class C {
+		int a;
+		private int _b;
+		@property int b() const { return _b; }
+		@property void b(int v) { _b = v; }
+
+		@property int test() const { return 10; }
+
+		void test2() {}
+	}
+	C c = new C;
+	c.a = 1;
+	c.b = 2;
+
+	C d;
+	deserializeJson(d, serializeToJson(c));
+	assert(c.a == d.a);
+	assert(c.b == d.b);
+}
+
+
+/**
+	Writes the given JSON object as a JSON string into the destination range.
+
+	This function will convert the given JSON value to a string without adding
+	any white space between tokens (no newlines, no indentation and no padding).
+	The output size is thus minizized, at the cost of bad human readability.
+
+	Params:
+		dst   = References the string output range to which the result is written.
+		json  = Specifies the JSON value that is to be stringified.
+
+	See_Also: Json.toString, writePrettyJsonString
+*/
+void writeJsonString(R)(ref R dst, in Json json)
+//	if( isOutputRange!R && is(ElementEncodingType!R == char) )
+{
+	final switch( json.type ){
+		case Json.Type.Undefined: dst.put("undefined"); break;
+		case Json.Type.Null: dst.put("null"); break;
+		case Json.Type.Bool: dst.put(cast(bool)json ? "true" : "false"); break;
+		case Json.Type.Int: formattedWrite(dst, "%d", json.get!long); break;
+		case Json.Type.Float: formattedWrite(dst, "%.16g", json.get!double); break;
+		case Json.Type.String:
+			dst.put("\"");
+			jsonEscape(dst, cast(string)json);
+			dst.put("\"");
+			break;
+		case Json.Type.Array:
+			dst.put("[");
+			bool first = true;
+			foreach( ref const Json e; json ){
+				if( e.type == Json.Type.Undefined ) continue;
+				if( !first ) dst.put(",");
+				first = false;
+				writeJsonString(dst, e);
+			}
+			dst.put("]");
+			break;
+		case Json.Type.Object:
+			dst.put("{");
+			bool first = true;
+			foreach( string k, ref const Json e; json ){
+				if( e.type == Json.Type.Undefined ) continue;
+				if( !first ) dst.put(",");
+				first = false;
+				dst.put("\"");
+				jsonEscape(dst, k);
+				dst.put("\":");
+				writeJsonString(dst, e);
+			}
+			dst.put("}");
+			break;
+	}
+}
+
+/**
+	Writes the given JSON object as a prettified JSON string into the destination range.
+
+	The output will contain newlines and indents to make the output human readable.
+
+	Params:
+		dst   = References the string output range to which the result is written.
+		json  = Specifies the JSON value that is to be stringified.
+		level = Specifies the base amount of indentation for the output. Indentation  is always
+		        done using tab characters.
+
+	See_Also: Json.toPrettyString, writeJsonString
+*/
+void writePrettyJsonString(R)(ref R dst, in Json json, int level = 0)
+//	if( isOutputRange!R && is(ElementEncodingType!R == char) )
+{
+	final switch( json.type ){
+		case Json.Type.Undefined: dst.put("undefined"); break;
+		case Json.Type.Null: dst.put("null"); break;
+		case Json.Type.Bool: dst.put(cast(bool)json ? "true" : "false"); break;
+		case Json.Type.Int: formattedWrite(dst, "%d", json.get!long); break;
+		case Json.Type.Float: formattedWrite(dst, "%.16g", json.get!double); break;
+		case Json.Type.String:
+			dst.put("\"");
+			jsonEscape(dst, cast(string)json);
+			dst.put("\"");
+			break;
+		case Json.Type.Array:
+			dst.put("[");
+			bool first = true;
+			foreach( e; json ){
+				if( e.type == Json.Type.Undefined ) continue;
+				if( !first ) dst.put(",");
+				first = false;
+				dst.put("\n");
+				foreach( tab; 0 .. level ) dst.put('\t');
+				writePrettyJsonString(dst, e, level+1);
+			}
+			if( json.length > 0 ) {
+				dst.put('\n');
+				foreach( tab; 0 .. (level-1) ) dst.put('\t');
+			}
+			dst.put("]");
+			break;
+		case Json.Type.Object:
+			dst.put("{");
+			bool first = true;
+			foreach( string k, e; json ){
+				if( e.type == Json.Type.Undefined ) continue;
+				if( !first ) dst.put(",");
+				dst.put("\n");
+				first = false;
+				foreach( tab; 0 .. level ) dst.put('\t');
+				dst.put("\"");
+				jsonEscape(dst, k);
+				dst.put("\": ");
+				writePrettyJsonString(dst, e, level+1);
+			}
+			if( json.length > 0 ) {
+				dst.put('\n');
+				foreach( tab; 0 .. (level-1) ) dst.put('\t');
+			}
+			dst.put("}");
+			break;
+	}
+}
+
+
+/** Deprecated aliases for backwards compatibility.
+
+	Use writeJsonString and writePrettyJsonString instead.
+*/
+deprecated("Please use writeJsonString instead.") alias writeJsonString toJson;
+///
+deprecated("Please use writePrettyJsonString instead.") alias writePrettyJsonString toPrettyJson;
+
+
+/// private
+private void jsonEscape(R)(ref R dst, string s)
+{
+	foreach( ch; s ){
+		switch(ch){
+			default: dst.put(ch); break;
+			case '\\': dst.put("\\\\"); break;
+			case '\r': dst.put("\\r"); break;
+			case '\n': dst.put("\\n"); break;
+			case '\t': dst.put("\\t"); break;
+			case '\"': dst.put("\\\""); break;
+		}
+	}
+}
+
+/// private
+private string jsonUnescape(R)(ref R range)
+{
+	auto ret = appender!string();
+	while(!range.empty){
+		auto ch = range.front;
+		switch( ch ){
+			case '"': return ret.data;
+			case '\\':
+				range.popFront();
+				enforce(!range.empty, "Unterminated string escape sequence.");
+				switch(range.front){
+					default: enforce("Invalid string escape sequence."); break;
+					case '"': ret.put('\"'); range.popFront(); break;
+					case '\\': ret.put('\\'); range.popFront(); break;
+					case '/': ret.put('/'); range.popFront(); break;
+					case 'b': ret.put('\b'); range.popFront(); break;
+					case 'f': ret.put('\f'); range.popFront(); break;
+					case 'n': ret.put('\n'); range.popFront(); break;
+					case 'r': ret.put('\r'); range.popFront(); break;
+					case 't': ret.put('\t'); range.popFront(); break;
+					case 'u':
+						range.popFront();
+						dchar uch = 0;
+						foreach( i; 0 .. 4 ){
+							uch *= 16;
+							enforce(!range.empty, "Unicode sequence must be '\\uXXXX'.");
+							auto dc = range.front;
+							range.popFront();
+							if( dc >= '0' && dc <= '9' ) uch += dc - '0';
+							else if( dc >= 'a' && dc <= 'f' ) uch += dc - 'a' + 10;
+							else if( dc >= 'A' && dc <= 'F' ) uch += dc - 'A' + 10;
+							else enforce(false, "Unicode sequence must be '\\uXXXX'.");
+						}
+						ret.put(uch);
+						break;
+				}
+				break;
+			default:
+				ret.put(ch);
+				range.popFront();
+				break;
+		}
+	}
+	return ret.data;
+}
+
+private string skipNumber(ref string s, out bool is_float)
+{
+	size_t idx = 0;
+	is_float = false;
+	if( s[idx] == '-' ) idx++;
+	if( s[idx] == '0' ) idx++;
+	else {
+		enforce(isDigit(s[idx++]), "Digit expected at beginning of number.");
+		while( idx < s.length && isDigit(s[idx]) ) idx++;
+	}
+
+	if( idx < s.length && s[idx] == '.' ){
+		idx++;
+		is_float = true;
+		while( idx < s.length && isDigit(s[idx]) ) idx++;
+	}
+
+	if( idx < s.length && (s[idx] == 'e' || s[idx] == 'E') ){
+		idx++;
+		is_float = true;
+		if( idx < s.length && (s[idx] == '+' || s[idx] == '-') ) idx++;
+		enforce( idx < s.length && isDigit(s[idx]), "Expected exponent." ~ s[0 .. idx]);
+		idx++;
+		while( idx < s.length && isDigit(s[idx]) ) idx++;
+	}
+
+	string ret = s[0 .. idx];
+	s = s[idx .. $];
+	return ret;
+}
+
+private string skipJsonString(ref string s, int* line = null)
+{
+	enforce(s.length >= 2 && s[0] == '\"', "too small: '" ~ s ~ "'");
+	s = s[1 .. $];
+	string ret = jsonUnescape(s);
+	enforce(s.length > 0 && s[0] == '\"', "Unterminated string literal.");
+	s = s[1 .. $];
+	return ret;
+}
+
+private void skipWhitespace(ref string s, int* line = null)
+{
+	while( s.length > 0 ){
+		switch( s[0] ){
+			default: return;
+			case ' ', '\t': s = s[1 .. $]; break;
+			case '\n':
+				s = s[1 .. $];
+				if( s.length > 0 && s[0] == '\r' ) s = s[1 .. $];
+				if( line ) (*line)++;
+				break;
+			case '\r':
+				s = s[1 .. $];
+				if( s.length > 0 && s[0] == '\n' ) s = s[1 .. $];
+				if( line ) (*line)++;
+				break;
+		}
+	}
+}
+
+/// private
+private bool isDigit(T)(T ch){ return ch >= '0' && ch <= '9'; }
+
+private string underscoreStrip(string field_name)
+{
+	if( field_name.length < 1 || field_name[$-1] != '_' ) return field_name;
+	else return field_name[0 .. $-1];
+}
diff --git a/source/vibe/data/utils.d b/source/vibe/data/utils.d
new file mode 100644
index 0000000..0f4cc99
--- /dev/null
+++ b/source/vibe/data/utils.d
@@ -0,0 +1,30 @@
+/**
+	Utility functions for data serialization
+
+	Copyright: © 2012 RejectedSoftware e.K.
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Sönke Ludwig
+*/
+module vibe.data.utils;
+
+public import std.traits;
+
+
+template isRWPlainField(T, string M)
+{
+	static if( !__traits(compiles, typeof(__traits(getMember, T, M))) ){
+		enum isRWPlainField = false;
+	} else {
+		//pragma(msg, T.stringof~"."~M~":"~typeof(__traits(getMember, T, M)).stringof);
+		enum isRWPlainField = isRWField!(T, M) && __traits(compiles, *(&__traits(getMember, Tgen!T(), M)) = *(&__traits(getMember, Tgen!T(), M)));
+	}
+}
+
+template isRWField(T, string M)
+{
+	enum isRWField = __traits(compiles, __traits(getMember, Tgen!T(), M) = __traits(getMember, Tgen!T(), M));
+	//pragma(msg, T.stringof~"."~M~": "~(isRWField?"1":"0"));
+}
+
+/// private
+private T Tgen(T)(){ return T.init; }
diff --git a/source/vibe/inet/path.d b/source/vibe/inet/path.d
new file mode 100644
index 0000000..b02b899
--- /dev/null
+++ b/source/vibe/inet/path.d
@@ -0,0 +1,295 @@
+/**
+	Contains routines for high level path handling.
+
+	Copyright: © 2012 RejectedSoftware e.K.
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Sönke Ludwig
+*/
+module vibe.inet.path;
+
+import std.algorithm;
+import std.array;
+import std.conv;
+import std.exception;
+import std.string;
+
+
+/**
+	Represents an absolute or relative file system path.
+
+	This struct allows to do safe operations on paths, such as concatenation and sub paths. Checks
+	are done to disallow invalid operations such as concatenating two absolute paths. It also
+	validates path strings and allows for easy checking of malicious relative paths.
+*/
+struct Path {
+	private {
+		immutable(PathEntry)[] m_nodes;
+		bool m_absolute = false;
+		bool m_endsWithSlash = false;
+	}
+	
+	/// Constructs a Path object by parsing a path string.
+	this(string pathstr)
+	{
+		m_nodes = cast(immutable)splitPath(pathstr);
+		m_absolute = (pathstr.startsWith("/") || m_nodes.length > 0 && m_nodes[0].toString().countUntil(':')>0);
+		m_endsWithSlash = pathstr.endsWith("/");
+		foreach( e; m_nodes ) assert(e.toString().length > 0);
+	}
+	
+	/// Constructs a path object from a list of PathEntry objects.
+	this(immutable(PathEntry)[] nodes, bool absolute)
+	{
+		m_nodes = nodes;
+		m_absolute = absolute;
+	}
+	
+	/// Constructs a relative path with one path entry.
+	this(PathEntry entry){
+		m_nodes = [entry];
+		m_absolute = false;
+	}
+	
+	/// Determines if the path is absolute.
+	@property bool absolute() const { return m_absolute; }
+
+	/// Resolves all '.' and '..' path entries as far as possible.
+	void normalize()
+	{
+		immutable(PathEntry)[] newnodes;
+		foreach( n; m_nodes ){
+			switch(n.toString()){
+				default:
+					newnodes ~= n;
+					break;
+				case ".": break;
+				case "..":
+					enforce(!m_absolute || newnodes.length > 0, "Path goes below root node.");
+					if( newnodes.length > 0 && newnodes[$-1] != ".." ) newnodes = newnodes[0 .. $-1];
+					else newnodes ~= n;
+					break;
+			}
+		}
+		m_nodes = newnodes;
+	}
+	
+	/// Converts the Path back to a string representation using slashes.
+	string toString()
+	const {
+		if( m_nodes.empty ) return absolute ? "/" : "";
+		
+		Appender!string ret;
+		
+		// for absolute paths start with /
+		if( absolute ) ret.put('/');
+		
+		foreach( i, f; m_nodes ){
+			if( i > 0 ) ret.put('/');
+			ret.put(f.toString());
+		}
+
+		if( m_nodes.length > 0 && m_endsWithSlash )
+			ret.put('/');
+		
+		return ret.data;
+	}
+	
+	/// Converts the Path object to a native path string (backslash as path separator on Windows).
+	string toNativeString()
+	const {
+		Appender!string ret;
+		
+		// for absolute unix paths start with /
+		version(Posix) { if(absolute) ret.put('/'); }
+		
+		foreach( i, f; m_nodes ){
+			version(Windows) { if( i > 0 ) ret.put('\\'); }
+			version(Posix) { if( i > 0 ) ret.put('/'); }
+			else { enforce("Unsupported OS"); }
+			ret.put(f.toString());
+		}
+		
+		if( m_nodes.length > 0 && m_endsWithSlash ){
+			version(Windows) { ret.put('\\'); }
+			version(Posix) { ret.put('/'); }
+		}
+		
+		return ret.data;
+	}
+	
+	/// Tests if `rhs` is an anchestor or the same as this path. 
+	bool startsWith(const Path rhs) const {
+		if( rhs.m_nodes.length > m_nodes.length ) return false;
+		foreach( i; 0 .. rhs.m_nodes.length )
+			if( m_nodes[i] != rhs.m_nodes[i] )
+				return false;
+		return true;
+	}
+	
+	/// Computes the relative path from `parentPath` to this path.
+	Path relativeTo(const Path parentPath) const {
+		int nup = 0;
+		while( parentPath.length > nup && !startsWith(parentPath[0 .. parentPath.length-nup]) ){
+			nup++;
+		}
+		Path ret = Path(null, false);
+		ret.m_endsWithSlash = true;
+		foreach( i; 0 .. nup ) ret ~= "..";
+		ret ~= Path(m_nodes[parentPath.length-nup .. $], false);
+		return ret;
+	}
+	
+	/// The last entry of the path
+	@property ref immutable(PathEntry) head() const { enforce(m_nodes.length > 0); return m_nodes[$-1]; }
+
+	/// The parent path
+	@property Path parentPath() const { return this[0 .. length-1]; }
+
+	/// The ist of path entries of which this path is composed
+	@property immutable(PathEntry)[] nodes() const { return m_nodes; }
+
+	/// The number of path entries of which this path is composed
+	@property size_t length() const { return m_nodes.length; }
+
+	/// True if the path contains no entries
+	@property bool empty() const { return m_nodes.length == 0; }
+
+	/// Determines if the path ends with a slash (i.e. is a directory)
+	@property bool endsWithSlash() const { return m_endsWithSlash; }
+	/// ditto
+	@property void endsWithSlash(bool v) { m_endsWithSlash = v; }
+
+	/// Determines if this path goes outside of its base path (i.e. begins with '..').
+	@property bool external() const { return !m_absolute && m_nodes.length > 0 && m_nodes[0].m_name == ".."; }
+		
+	ref immutable(PathEntry) opIndex(size_t idx) const { return m_nodes[idx]; }
+	Path opSlice(size_t start, size_t end) const {
+		auto ret = Path(m_nodes[start .. end], start == 0 ? absolute : false);
+		if( end == m_nodes.length ) ret.m_endsWithSlash = m_endsWithSlash;
+		return ret;
+	}
+	size_t opDollar(int dim)() const if(dim == 0) { return m_nodes.length; }
+	
+	
+	Path opBinary(string OP)(const Path rhs) const if( OP == "~" ) {
+		Path ret;
+		ret.m_nodes = m_nodes;
+		ret.m_absolute = m_absolute;
+		ret.m_endsWithSlash = rhs.m_endsWithSlash;
+		ret.normalize(); // needed to avoid "."~".." become "" instead of ".."
+		
+		assert(!rhs.absolute, "Trying to append absolute path.");
+		size_t idx = m_nodes.length;
+		foreach(folder; rhs.m_nodes){
+			switch(folder.toString()){
+				default: ret.m_nodes = ret.m_nodes ~ folder; break;
+				case ".": break;
+				case "..":
+					enforce(!ret.absolute || ret.m_nodes.length > 0, "Relative path goes below root node!");
+					if( ret.m_nodes.length > 0 && ret.m_nodes[$-1].toString() != ".." )
+						ret.m_nodes = ret.m_nodes[0 .. $-1];
+					else ret.m_nodes = ret.m_nodes ~ folder;
+					break;
+			}
+		}
+		return ret;
+	}
+	
+	Path opBinary(string OP)(string rhs) const if( OP == "~" ) { assert(rhs.length > 0); return opBinary!"~"(Path(rhs)); }
+	Path opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { assert(rhs.toString().length > 0); return opBinary!"~"(Path(rhs)); }
+	void opOpAssign(string OP)(string rhs) if( OP == "~" ) { assert(rhs.length > 0); opOpAssign!"~"(Path(rhs)); }
+	void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { assert(rhs.toString().length > 0); opOpAssign!"~"(Path(rhs)); }
+	void opOpAssign(string OP)(Path rhs) if( OP == "~" ) { auto p = this ~ rhs; m_nodes = p.m_nodes; m_endsWithSlash = rhs.m_endsWithSlash; }
+	
+	/// Tests two paths for equality using '=='.
+	bool opEquals(ref const Path rhs) const {
+		if( m_absolute != rhs.m_absolute ) return false;
+		if( m_endsWithSlash != rhs.m_endsWithSlash ) return false;
+		if( m_nodes.length != rhs.length ) return false;
+		foreach( i; 0 .. m_nodes.length )
+			if( m_nodes[i] != rhs.m_nodes[i] )
+				return false;
+		return true;
+	}
+	/// ditto
+	bool opEquals(const Path other) const { return opEquals(other); }
+
+	int opCmp(ref const Path rhs) const {
+		if( m_absolute != rhs.m_absolute ) return cast(int)m_absolute - cast(int)rhs.m_absolute;
+		foreach( i; 0 .. min(m_nodes.length, rhs.m_nodes.length) )
+			if( m_nodes[i] != rhs.m_nodes[i] )
+				return m_nodes[i].opCmp(rhs.m_nodes[i]);
+		if( m_nodes.length > rhs.m_nodes.length ) return 1;
+		if( m_nodes.length < rhs.m_nodes.length ) return -1;
+		return 0;
+	}
+}
+
+struct PathEntry {
+	private {
+		string m_name;
+	}
+	
+	this(string str)
+	{
+		assert(str.countUntil('/') < 0 && str.countUntil('\\') < 0);
+		m_name = str;
+	}
+	
+	string toString() const { return m_name; }
+
+	Path opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return Path(cast(immutable)[this, rhs], false); }
+	
+	bool opEquals(ref const PathEntry rhs) const { return m_name == rhs.m_name; }
+	bool opEquals(PathEntry rhs) const { return m_name == rhs.m_name; }
+	bool opEquals(string rhs) const { return m_name == rhs; }
+	int opCmp(ref const PathEntry rhs) const { return m_name.cmp(rhs.m_name); }
+	int opCmp(string rhs) const { return m_name.cmp(rhs); }
+}
+
+private bool isValidFilename(string str)
+{
+	foreach( ch; str )
+		if( ch == '/' || /*ch == ':' ||*/ ch == '\\' ) return false;
+	return true;
+}
+
+/// Joins two path strings. subpath must be relative.
+string joinPath(string basepath, string subpath)
+{
+	Path p1 = Path(basepath);
+	Path p2 = Path(subpath);
+	return (p1 ~ p2).toString();
+}
+
+/// Splits up a path string into its elements/folders
+PathEntry[] splitPath(string path)
+{
+	if( path.startsWith("/") || path.startsWith("\\") ) path = path[1 .. $];
+	if( path.empty ) return null;
+	if( path.endsWith("/") || path.endsWith("\\") ) path = path[0 .. $-1];
+
+	// count the number of path nodes
+	size_t nelements = 0;
+	foreach( i, char ch; path )
+		if( ch == '\\' || ch == '/' )
+			nelements++;
+	nelements++;
+	
+	// reserve space for the elements
+	auto elements = new PathEntry[nelements];
+
+	// read and return the elements
+	size_t startidx = 0;
+	size_t eidx = 0;
+	foreach( i, char ch; path )
+		if( ch == '\\' || ch == '/' ){
+			enforce(i - startidx > 0, "Empty path entries not allowed.");
+			elements[eidx++] = PathEntry(path[startidx .. i]);
+			startidx = i+1;
+		}
+	elements[eidx++] = PathEntry(path[startidx .. $]);
+	enforce(path.length - startidx > 0, "Empty path entries not allowed.");
+	assert(eidx == nelements);
+	return elements;
+}
diff --git a/source/vibe/inet/url.d b/source/vibe/inet/url.d
new file mode 100644
index 0000000..2e6b1fa
--- /dev/null
+++ b/source/vibe/inet/url.d
@@ -0,0 +1,277 @@
+/**
+	URL parsing routines.
+
+	Copyright: © 2012 RejectedSoftware e.K.
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Sönke Ludwig
+*/
+module vibe.inet.url;
+
+public import vibe.inet.path;
+
+import std.algorithm;
+import std.array;
+import std.conv;
+import std.exception;
+import std.string;
+import std.uri;
+
+
+/**
+	Represents a URL decomposed into its components.
+*/
+struct Url {
+	private {
+		string m_schema;
+		string m_pathString;
+		Path m_path;
+		string m_host;
+		ushort m_port;
+		string m_username;
+		string m_password;
+		string m_queryString;
+		string m_anchor;
+	}
+
+	/// Constructs a new URL object from its components.
+	this(string schema, string host, ushort port, Path path)
+	{
+		m_schema = schema;
+		m_host = host;
+		m_port = port;
+		m_path = path;
+		m_pathString = path.toString();
+	}
+	/// ditto
+	this(string schema, Path path)
+	{
+		this(schema, null, 0, path);
+	}
+
+	/** Constructs a URL from its string representation.
+	
+		TODO: additional validation required (e.g. valid host and user names and port)
+	*/
+	this(string url_string)
+	{
+		auto str = url_string;
+		enforce(str.length > 0, "Empty URL.");
+		if( str[0] != '/' ){
+			auto idx = str.countUntil(':');
+			enforce(idx > 0, "No schema in URL:"~str);
+			m_schema = str[0 .. idx];
+			str = str[idx+1 .. $];
+			bool requires_host = false;
+
+			switch(m_schema){
+				case "http":
+				case "https":
+				case "ftp":
+				case "spdy":
+				case "sftp":
+				case "file":
+					// proto://server/path style
+					enforce(str.startsWith("//"), "URL must start with proto://...");
+					requires_host = true;
+					str = str[2 .. $];
+					goto default;
+				default:
+					auto si = str.countUntil('/');
+					if( si < 0 ) si = str.length;
+					auto ai = str[0 .. si].countUntil('@');
+					sizediff_t hs = 0;
+					if( ai >= 0 ){
+						hs = ai+1;
+						auto ci = str[0 .. ai].countUntil(':');
+						if( ci >= 0 ){
+							m_username = str[0 .. ci];
+							m_password = str[ci+1 .. ai];
+						} else m_username = str[0 .. ai];
+						enforce(m_username.length > 0, "Empty user name in URL.");
+					}
+
+					m_host = str[hs .. si];
+					auto pi = m_host.countUntil(':');
+					if(pi > 0) {
+						enforce(pi < m_host.length-1, "Empty port in URL.");
+						m_port = to!ushort(m_host[pi+1..$]);
+						m_host = m_host[0 .. pi];
+					}
+
+					enforce(!requires_host || m_schema == "file" || m_host.length > 0,
+							"Empty server name in URL.");
+					str = str[si .. $];
+			}
+		}
+
+		this.localURI = str;
+	}
+	/// ditto
+	static Url parse(string url_string)
+	{
+		return Url(url_string);
+	}
+
+	/// The schema/protocol part of the URL
+	@property string schema() const { return m_schema; }
+	/// ditto
+	@property void schema(string v) { m_schema = v; }
+
+	/// The path part of the URL in the original string form
+	@property string pathString() const { return m_pathString; }
+
+	/// The path part of the URL
+	@property Path path() const { return m_path; }
+	/// ditto
+	@property void path(Path p)
+	{
+		m_path = p;
+		auto pstr = p.toString();
+		m_pathString = pstr;
+	}
+
+	/// The host part of the URL (depends on the schema)
+	@property string host() const { return m_host; }
+	/// ditto
+	@property void host(string v) { m_host = v; }
+
+	/// The port part of the URL (optional)
+	@property ushort port() const { return m_port; }
+	/// ditto
+	@property port(ushort v) { m_port = v; }
+
+	/// The user name part of the URL (optional)
+	@property string username() const { return m_username; }
+	/// ditto
+	@property void username(string v) { m_username = v; }
+
+	/// The password part of the URL (optional)
+	@property string password() const { return m_password; }
+	/// ditto
+	@property void password(string v) { m_password = v; }
+
+	/// The query string part of the URL (optional)
+	@property string queryString() const { return m_queryString; }
+	/// ditto
+	@property void queryString(string v) { m_queryString = v; }
+
+	/// The anchor part of the URL (optional)
+	@property string anchor() const { return m_anchor; }
+
+	/// The path part plus query string and anchor
+	@property string localURI()
+	const { 
+		auto str = appender!string();
+		str.reserve(m_pathString.length + 2 + queryString.length + anchor.length);
+		str.put(encodeComponent(path.toString()));
+		if( queryString.length ) {
+			str.put("?");
+			str.put(queryString);
+		} 
+		if( anchor.length ) {
+			str.put("#");
+			str.put(anchor);
+		}
+		return str.data;
+	}
+	/// ditto
+	@property void localURI(string str)
+	{
+		auto ai = str.countUntil('#');
+		if( ai >= 0 ){
+			m_anchor = str[ai+1 .. $];
+			str = str[0 .. ai];
+		}
+
+		auto qi = str.countUntil('?');
+		if( qi >= 0 ){
+			m_queryString = str[qi+1 .. $];
+			str = str[0 .. qi];
+		}
+
+		m_pathString = str;
+		m_path = Path(decodeComponent(str));
+	}
+
+	/// The URL to the parent path with query string and anchor stripped.
+	@property Url parentUrl() const {
+		Url ret;
+		ret.schema = schema;
+		ret.host = host;
+		ret.port = port;
+		ret.username = username;
+		ret.password = password;
+		ret.path = path.parentPath;
+		return ret;
+	}
+
+	/// Converts this URL object to its string representation.
+	string toString()
+	const {
+		import std.format;
+		auto dst = appender!string();
+		dst.put(schema);
+		dst.put(":");
+		switch(schema){
+			default: break;
+			case "file":
+			case "http":
+			case "https":
+			case "ftp":
+			case "spdy":
+			case "sftp":
+				dst.put("//");
+				break;
+		}
+		dst.put(host);
+		if( m_port > 0 ) formattedWrite(dst, ":%d", m_port);
+		dst.put(localURI);
+		return dst.data;
+	}
+
+	bool startsWith(const Url rhs) const {
+		if( m_schema != rhs.m_schema ) return false;
+		if( m_host != rhs.m_host ) return false;
+		// FIXME: also consider user, port, querystring, anchor etc
+		return path.startsWith(rhs.m_path);
+	}
+
+	Url opBinary(string OP)(Path rhs) const if( OP == "~" ) { return Url(m_schema, m_host, m_port, m_path ~ rhs); }
+	Url opBinary(string OP)(PathEntry rhs) const if( OP == "~" ) { return Url(m_schema, m_host, m_port, m_path ~ rhs); }
+	void opOpAssign(string OP)(Path rhs) if( OP == "~" ) { m_path ~= rhs; }
+	void opOpAssign(string OP)(PathEntry rhs) if( OP == "~" ) { m_path ~= rhs; }
+
+	/// Tests two URLs for equality using '=='.
+	bool opEquals(ref const Url rhs) const {
+		if( m_schema != rhs.m_schema ) return false;
+		if( m_host != rhs.m_host ) return false;
+		if( m_path != rhs.m_path ) return false;
+		return true;
+	}
+	/// ditto
+	bool opEquals(const Url other) const { return opEquals(other); }
+
+	int opCmp(ref const Url rhs) const {
+		if( m_schema != rhs.m_schema ) return m_schema.cmp(rhs.m_schema);
+		if( m_host != rhs.m_host ) return m_host.cmp(rhs.m_host);
+		if( m_path != rhs.m_path ) return m_path.opCmp(rhs.m_path);
+		return true;
+	}
+}
+
+unittest {
+	auto url = Url.parse("https://www.example.net/index.html");
+	assert(url.schema == "https", url.schema);
+	assert(url.host == "www.example.net", url.host);
+	assert(url.path == Path("/index.html"), url.path.toString());
+	
+	url = Url.parse("http://jo.doe:password@sub.www.example.net:4711/sub2/index.html?query#anchor");
+	assert(url.schema == "http", url.schema);
+	assert(url.username == "jo.doe", url.username);
+	assert(url.password == "password", url.password);
+	assert(url.port == 4711, to!string(url.port));
+	assert(url.host == "sub.www.example.net", url.host);
+	assert(url.path.toString() == "/sub2/index.html", url.path.toString());
+	assert(url.queryString == "query", url.queryString);
+	assert(url.anchor == "anchor", url.anchor);
+}
diff --git a/source/vibe/inet/urltransfer.d b/source/vibe/inet/urltransfer.d
new file mode 100644
index 0000000..eddad5d
--- /dev/null
+++ b/source/vibe/inet/urltransfer.d
@@ -0,0 +1,34 @@
+/**
+	Downloading and uploading of data from/to URLs.
+
+	Copyright: © 2012 RejectedSoftware e.K.
+	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
+	Authors: Sönke Ludwig
+*/
+module vibe.inet.urltransfer;
+
+import vibe.core.log;
+import vibe.core.file;
+import vibe.inet.url;
+
+import std.exception;
+import std.net.curl;
+import std.string;
+
+
+/**
+	Downloads a file from the specified URL.
+
+	Any redirects will be followed until the actual file resource is reached or if the redirection
+	limit of 10 is reached. Note that only HTTP(S) is currently supported.
+*/
+void download(string url, string filename)
+{
+	std.net.curl.download(url, filename);
+}
+
+/// ditto
+void download(Url url, Path filename)
+{
+	download(url.toString(), filename.toNativeString());
+}