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()); +}