summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEdward Thomson <ethomson@edwardthomson.com>2023-02-15 21:25:19 +0000
committerEdward Thomson <ethomson@edwardthomson.com>2023-05-17 13:11:46 +0100
commitd3ed832b1fec7017aed261df768254f4a86e7f6d (patch)
treeaa41db497840d0784a4ad72007f3656780a0e9ee
parent46c3bdc6f065848977b592396ad730056a2c2cf8 (diff)
downloadlibgit2-d3ed832b1fec7017aed261df768254f4a86e7f6d.tar.gz
Introduce git_process class that invokes processes
-rw-r--r--src/util/process.h157
-rw-r--r--src/util/unix/process.c499
-rw-r--r--src/util/win32/process.c456
-rw-r--r--tests/resources/process/cat.bat2
-rw-r--r--tests/resources/process/env.cmd2
-rwxr-xr-xtests/resources/process/helloworld.sh3
-rw-r--r--tests/resources/process/pwd.bat2
-rw-r--r--tests/util/process/env.c111
-rw-r--r--tests/util/process/start.c212
-rw-r--r--tests/util/process/win32.c64
10 files changed, 1508 insertions, 0 deletions
diff --git a/src/util/process.h b/src/util/process.h
new file mode 100644
index 000000000..59ea52d91
--- /dev/null
+++ b/src/util/process.h
@@ -0,0 +1,157 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+
+#ifndef INCLUDE_process_h__
+#define INCLUDE_process_h__
+
+typedef struct git_process git_process;
+
+typedef struct {
+ int capture_in : 1,
+ capture_out : 1,
+ capture_err : 1,
+ exclude_env : 1;
+
+ char *cwd;
+} git_process_options;
+
+typedef enum {
+ GIT_PROCESS_STATUS_NONE,
+ GIT_PROCESS_STATUS_NORMAL,
+ GIT_PROCESS_STATUS_ERROR
+} git_process_result_status;
+
+#define GIT_PROCESS_RESULT_INIT { GIT_PROCESS_STATUS_NONE }
+
+typedef struct {
+ git_process_result_status status;
+ int exitcode;
+ int signal;
+} git_process_result;
+
+#define GIT_PROCESS_OPTIONS_INIT { 0 }
+
+/**
+ * Create a new process. The command to run should be specified as the
+ * element of the `arg` array. If `setup_pipe` is true, then this
+ * process can be written to and its output read from.
+ *
+ * This function will add the given environment variables (in `env`)
+ * to the current environment. Operations on environment variables
+ * are not thread safe, so you may not modify the environment during
+ * this call. You can avoid this by setting `exclude_env` in the
+ * options and providing the entire environment yourself.
+ *
+ * @param out location to store the process
+ * @param args the command (with arguments) to run
+ * @param args_len the length of the args array
+ * @param env environment variables to add (or NULL)
+ * @param env_len the length of the env len
+ * @param opts the options for creating the process
+ * @return 0 or an error code
+ */
+extern int git_process_new(
+ git_process **out,
+ const char **args,
+ size_t args_len,
+ const char **env,
+ size_t env_len,
+ git_process_options *opts);
+
+#ifdef GIT_WIN32
+
+/* Windows path parsing is tricky; this helper function is for testing. */
+extern int git_process__cmdline(
+ git_str *out,
+ const char **in,
+ size_t in_len);
+
+#endif
+
+/**
+ * Start the process.
+ *
+ * @param process the process to start
+ * @return 0 or an error code
+ */
+extern int git_process_start(git_process *process);
+
+/**
+ * Read from the process's stdout. The process must have been created with
+ * `capture_out` set to true.
+ *
+ * @param process the process to read from
+ * @param buf the buf to read into
+ * @param count maximum number of bytes to read
+ * @return number of bytes read or an error code
+ */
+extern ssize_t git_process_read(git_process *process, void *buf, size_t count);
+
+/**
+ * Write to the process's stdin. The process must have been created with
+ * `capture_in` set to true.
+ *
+ * @param process the process to write to
+ * @param buf the buf to write
+ * @param count maximum number of bytes to write
+ * @return number of bytes written or an error code
+ */
+extern ssize_t git_process_write(git_process *process, const void *buf, size_t count);
+
+/**
+ * Wait for the process to finish.
+ *
+ * @param result the result of the process or NULL
+ * @param process the process to wait on
+ */
+extern int git_process_wait(git_process_result *result, git_process *process);
+
+/**
+ * Close the input pipe from the child.
+ *
+ * @param process the process to close the pipe on
+ */
+extern int git_process_close_in(git_process *process);
+
+/**
+ * Close the output pipe from the child.
+ *
+ * @param process the process to close the pipe on
+ */
+extern int git_process_close_out(git_process *process);
+
+/**
+ * Close the error pipe from the child.
+ *
+ * @param process the process to close the pipe on
+ */
+extern int git_process_close_err(git_process *process);
+
+/**
+ * Close all resources that are used by the process. This does not
+ * wait for the process to complete.
+ *
+ * @parma process the process to close
+ */
+extern int git_process_close(git_process *process);
+
+/**
+ * Place a human-readable error message in the given git buffer.
+ *
+ * @param msg the buffer to store the message
+ * @param result the process result that produced an error
+ */
+extern int git_process_result_msg(git_str *msg, git_process_result *result);
+
+/**
+ * Free a process structure
+ *
+ * @param process the process to free
+ */
+extern void git_process_free(git_process *process);
+
+#endif
diff --git a/src/util/unix/process.c b/src/util/unix/process.c
new file mode 100644
index 000000000..16171aec9
--- /dev/null
+++ b/src/util/unix/process.c
@@ -0,0 +1,499 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+
+#include <stdio.h>
+#include <sys/wait.h>
+#include <git2.h>
+
+#include "git2_util.h"
+#include "vector.h"
+#include "process.h"
+#include "strlist.h"
+
+extern char **environ;
+
+struct git_process {
+ char **args;
+ char **env;
+
+ char *cwd;
+
+ unsigned int capture_in : 1,
+ capture_out : 1,
+ capture_err : 1;
+
+ pid_t pid;
+
+ int child_in;
+ int child_out;
+ int child_err;
+ git_process_result_status status;
+};
+
+GIT_INLINE(bool) is_delete_env(const char *env)
+{
+ char *c = index(env, '=');
+
+ if (c == NULL)
+ return false;
+
+ return *(c+1) == '\0';
+}
+
+static int merge_env(
+ char ***out,
+ const char **env,
+ size_t env_len,
+ bool exclude_env)
+{
+ git_vector merged = GIT_VECTOR_INIT;
+ char **kv, *dup;
+ size_t max, cnt;
+ int error = 0;
+
+ for (max = env_len, kv = environ; !exclude_env && *kv; kv++)
+ max++;
+
+ if ((error = git_vector_init(&merged, max, NULL)) < 0)
+ goto on_error;
+
+ for (cnt = 0; env && cnt < env_len; cnt++) {
+ if (is_delete_env(env[cnt]))
+ continue;
+
+ dup = git__strdup(env[cnt]);
+ GIT_ERROR_CHECK_ALLOC(dup);
+
+ if ((error = git_vector_insert(&merged, dup)) < 0)
+ goto on_error;
+ }
+
+ if (!exclude_env) {
+ for (kv = environ; *kv; kv++) {
+ if (env && git_strlist_contains_key(env, env_len, *kv, '='))
+ continue;
+
+ dup = git__strdup(*kv);
+ GIT_ERROR_CHECK_ALLOC(dup);
+
+ if ((error = git_vector_insert(&merged, dup)) < 0)
+ goto on_error;
+ }
+ }
+
+ if (merged.length == 0) {
+ *out = NULL;
+ error = 0;
+ goto on_error;
+ }
+
+ git_vector_insert(&merged, NULL);
+
+ *out = (char **)merged.contents;
+
+ return 0;
+
+on_error:
+ git_vector_free_deep(&merged);
+ return error;
+}
+
+int git_process_new(
+ git_process **out,
+ const char **args,
+ size_t args_len,
+ const char **env,
+ size_t env_len,
+ git_process_options *opts)
+{
+ git_process *process;
+
+ GIT_ASSERT_ARG(out && args && args_len > 0);
+
+ *out = NULL;
+
+ process = git__calloc(sizeof(git_process), 1);
+ GIT_ERROR_CHECK_ALLOC(process);
+
+ if (git_strlist_copy_with_null(&process->args, args, args_len) < 0 ||
+ merge_env(&process->env, env, env_len, opts->exclude_env) < 0) {
+ git_process_free(process);
+ return -1;
+ }
+
+ if (opts) {
+ process->capture_in = opts->capture_in;
+ process->capture_out = opts->capture_out;
+ process->capture_err = opts->capture_err;
+
+ if (opts->cwd) {
+ process->cwd = git__strdup(opts->cwd);
+ GIT_ERROR_CHECK_ALLOC(process->cwd);
+ }
+ }
+
+ process->child_in = -1;
+ process->child_out = -1;
+ process->child_err = -1;
+ process->status = -1;
+
+ *out = process;
+ return 0;
+}
+
+#define CLOSE_FD(fd) \
+ if (fd >= 0) { \
+ close(fd); \
+ fd = -1; \
+ }
+
+static int try_read(size_t *out, int fd, void *buf, size_t len)
+{
+ size_t read_len = 0;
+ int ret = -1;
+
+ while (ret && read_len < len) {
+ ret = read(fd, buf + read_len, len - read_len);
+
+ if (ret < 0 && errno != EAGAIN && errno != EINTR) {
+ git_error_set(GIT_ERROR_OS, "could not read child status");
+ return -1;
+ }
+
+ read_len += ret;
+ }
+
+ *out = read_len;
+ return 0;
+}
+
+
+static int read_status(int fd)
+{
+ size_t status_len = sizeof(int) * 3, read_len = 0;
+ char buffer[status_len], fn[128];
+ int error, fn_error, os_error, fn_len = 0;
+
+ if ((error = try_read(&read_len, fd, buffer, status_len)) < 0)
+ return error;
+
+ /* Immediate EOF indicates the exec succeeded. */
+ if (read_len == 0)
+ return 0;
+
+ if (read_len < status_len) {
+ git_error_set(GIT_ERROR_INVALID, "child status truncated");
+ return -1;
+ }
+
+ memcpy(&fn_error, &buffer[0], sizeof(int));
+ memcpy(&os_error, &buffer[sizeof(int)], sizeof(int));
+ memcpy(&fn_len, &buffer[sizeof(int) * 2], sizeof(int));
+
+ if (fn_len > 0) {
+ fn_len = min(fn_len, (int)(ARRAY_SIZE(fn) - 1));
+
+ if ((error = try_read(&read_len, fd, fn, fn_len)) < 0)
+ return error;
+
+ fn[fn_len] = '\0';
+ } else {
+ fn[0] = '\0';
+ }
+
+ if (fn_error) {
+ errno = os_error;
+ git_error_set(GIT_ERROR_OS, "could not %s", fn[0] ? fn : "(unknown)");
+ }
+
+ return fn_error;
+}
+
+static bool try_write(int fd, const void *buf, size_t len)
+{
+ size_t write_len;
+ int ret;
+
+ for (write_len = 0; write_len < len; ) {
+ ret = write(fd, buf + write_len, len - write_len);
+
+ if (ret <= 0)
+ break;
+
+ write_len += ret;
+ }
+
+ return (len == write_len);
+}
+
+static void write_status(int fd, const char *fn, int error, int os_error)
+{
+ size_t status_len = sizeof(int) * 3, fn_len;
+ char buffer[status_len];
+
+ fn_len = strlen(fn);
+
+ if (fn_len > INT_MAX)
+ fn_len = INT_MAX;
+
+ memcpy(&buffer[0], &error, sizeof(int));
+ memcpy(&buffer[sizeof(int)], &os_error, sizeof(int));
+ memcpy(&buffer[sizeof(int) * 2], &fn_len, sizeof(int));
+
+ /* Do our best effort to write all the status. */
+ if (!try_write(fd, buffer, status_len))
+ return;
+
+ if (fn_len)
+ try_write(fd, fn, fn_len);
+}
+
+int git_process_start(git_process *process)
+{
+ int in[2] = { -1, -1 }, out[2] = { -1, -1 },
+ err[2] = { -1, -1 }, status[2] = { -1, -1 };
+ int fdflags, state, error;
+ pid_t pid;
+
+ /* Set up the pipes to read from/write to the process */
+ if ((process->capture_in && pipe(in) < 0) ||
+ (process->capture_out && pipe(out) < 0) ||
+ (process->capture_err && pipe(err) < 0)) {
+ git_error_set(GIT_ERROR_OS, "could not create pipe");
+ goto on_error;
+ }
+
+ /* Set up a self-pipe for status from the forked process. */
+ if (pipe(status) < 0 ||
+ (fdflags = fcntl(status[1], F_GETFD)) < 0 ||
+ fcntl(status[1], F_SETFD, fdflags | FD_CLOEXEC) < 0) {
+ git_error_set(GIT_ERROR_OS, "could not create pipe");
+ goto on_error;
+ }
+
+ switch (pid = fork()) {
+ case -1:
+ git_error_set(GIT_ERROR_OS, "could not fork");
+ goto on_error;
+
+ /* Child: start the process. */
+ case 0:
+ /* Close the opposing side of the pipes */
+ CLOSE_FD(status[0]);
+
+ if (process->capture_in) {
+ CLOSE_FD(in[1]);
+ dup2(in[0], STDIN_FILENO);
+ }
+
+ if (process->capture_out) {
+ CLOSE_FD(out[0]);
+ dup2(out[1], STDOUT_FILENO);
+ }
+
+ if (process->capture_err) {
+ CLOSE_FD(err[0]);
+ dup2(err[1], STDERR_FILENO);
+ }
+
+ if (process->cwd && (error = chdir(process->cwd)) < 0) {
+ write_status(status[1], "chdir", error, errno);
+ exit(0);
+ }
+
+ /*
+ * Exec the process and write the results back if the
+ * call fails. If it succeeds, we'll close the status
+ * pipe (via CLOEXEC) and the parent will know.
+ */
+ error = execve(process->args[0],
+ process->args,
+ process->env);
+
+ write_status(status[1], "execve", error, errno);
+ exit(0);
+
+ /* Parent: make sure the child process exec'd correctly. */
+ default:
+ /* Close the opposing side of the pipes */
+ CLOSE_FD(status[1]);
+
+ if (process->capture_in) {
+ CLOSE_FD(in[0]);
+ process->child_in = in[1];
+ }
+
+ if (process->capture_out) {
+ CLOSE_FD(out[1]);
+ process->child_out = out[0];
+ }
+
+ if (process->capture_err) {
+ CLOSE_FD(err[1]);
+ process->child_err = err[0];
+ }
+
+ /* Try to read the status */
+ process->status = status[0];
+ if ((error = read_status(status[0])) < 0) {
+ waitpid(process->pid, &state, 0);
+ goto on_error;
+ }
+
+ process->pid = pid;
+ return 0;
+ }
+
+on_error:
+ CLOSE_FD(in[0]); CLOSE_FD(in[1]);
+ CLOSE_FD(out[0]); CLOSE_FD(out[1]);
+ CLOSE_FD(err[0]); CLOSE_FD(err[1]);
+ CLOSE_FD(status[0]); CLOSE_FD(status[1]);
+ return -1;
+}
+
+ssize_t git_process_read(git_process *process, void *buf, size_t count)
+{
+ ssize_t ret;
+
+ GIT_ASSERT_ARG(process);
+ GIT_ASSERT(process->capture_out);
+
+ if (count > SSIZE_MAX)
+ count = SSIZE_MAX;
+
+ if ((ret = read(process->child_out, buf, count)) < 0) {
+ git_error_set(GIT_ERROR_OS, "could not read from child process");
+ return -1;
+ }
+
+ return ret;
+}
+
+ssize_t git_process_write(git_process *process, const void *buf, size_t count)
+{
+ ssize_t ret;
+
+ GIT_ASSERT_ARG(process);
+ GIT_ASSERT(process->capture_in);
+
+ if (count > SSIZE_MAX)
+ count = SSIZE_MAX;
+
+ if ((ret = write(process->child_in, buf, count)) < 0) {
+ git_error_set(GIT_ERROR_OS, "could not write to child process");
+ return -1;
+ }
+
+ return ret;
+}
+
+int git_process_close_in(git_process *process)
+{
+ if (!process->capture_in) {
+ git_error_set(GIT_ERROR_INVALID, "input is not open");
+ return -1;
+ }
+
+ CLOSE_FD(process->child_in);
+ return 0;
+}
+
+int git_process_close_out(git_process *process)
+{
+ if (!process->capture_out) {
+ git_error_set(GIT_ERROR_INVALID, "output is not open");
+ return -1;
+ }
+
+ CLOSE_FD(process->child_out);
+ return 0;
+}
+
+int git_process_close_err(git_process *process)
+{
+ if (!process->capture_err) {
+ git_error_set(GIT_ERROR_INVALID, "error is not open");
+ return -1;
+ }
+
+ CLOSE_FD(process->child_err);
+ return 0;
+}
+
+int git_process_close(git_process *process)
+{
+ CLOSE_FD(process->child_in);
+ CLOSE_FD(process->child_out);
+ CLOSE_FD(process->child_err);
+ CLOSE_FD(process->status);
+
+ return 0;
+}
+
+int git_process_wait(git_process_result *result, git_process *process)
+{
+ int state;
+
+ if (result)
+ memset(result, 0, sizeof(git_process_result));
+
+ if (!process->pid) {
+ git_error_set(GIT_ERROR_INVALID, "process is stopped");
+ return -1;
+ }
+
+ if (waitpid(process->pid, &state, 0) < 0) {
+ git_error_set(GIT_ERROR_OS, "could not wait for child");
+ return -1;
+ }
+
+ process->pid = 0;
+
+ if (result) {
+ if (WIFEXITED(state)) {
+ result->status = GIT_PROCESS_STATUS_NORMAL;
+ result->exitcode = WEXITSTATUS(state);
+ } else if (WIFSIGNALED(state)) {
+ result->status = GIT_PROCESS_STATUS_ERROR;
+ result->signal = WTERMSIG(state);
+ } else {
+ result->status = GIT_PROCESS_STATUS_ERROR;
+ }
+ }
+
+ return 0;
+}
+
+int git_process_result_msg(git_str *out, git_process_result *result)
+{
+ if (result->status == GIT_PROCESS_STATUS_NONE) {
+ return git_str_puts(out, "process not started");
+ } else if (result->status == GIT_PROCESS_STATUS_NORMAL) {
+ return git_str_printf(out, "process exited with code %d",
+ result->exitcode);
+ } else if (result->signal) {
+ return git_str_printf(out, "process exited on signal %d",
+ result->signal);
+ }
+
+ return git_str_puts(out, "unknown error");
+}
+
+void git_process_free(git_process *process)
+{
+ if (!process)
+ return;
+
+ if (process->pid)
+ git_process_close(process);
+
+ git__free(process->cwd);
+ git_strlist_free_with_null(process->args);
+ git_strlist_free_with_null(process->env);
+ git__free(process);
+}
diff --git a/src/util/win32/process.c b/src/util/win32/process.c
new file mode 100644
index 000000000..6a1dd658c
--- /dev/null
+++ b/src/util/win32/process.c
@@ -0,0 +1,456 @@
+/*
+ * Copyright (C) the libgit2 contributors. All rights reserved.
+ *
+ * This file is part of libgit2, distributed under the GNU GPL v2 with
+ * a Linking Exception. For full terms see the included COPYING file.
+ */
+
+#include <stdio.h>
+#include <git2.h>
+
+#include "git2_util.h"
+#include "process.h"
+#include "git2/strarray.h"
+
+#define ENV_MAX 32767
+
+struct git_process {
+ wchar_t *appname;
+ wchar_t *cmdline;
+ wchar_t *env;
+
+ wchar_t *cwd;
+
+ unsigned int capture_in : 1,
+ capture_out : 1,
+ capture_err : 1;
+
+ PROCESS_INFORMATION process_info;
+
+ HANDLE child_in;
+ HANDLE child_out;
+ HANDLE child_err;
+
+ git_process_result_status status;
+};
+
+/*
+ * Windows processes have a single command-line that is split by the
+ * invoked application into arguments (instead of an array of
+ * command-line arguments). This command-line is split by space or
+ * tab delimiters, unless that whitespace is within a double quote.
+ * Literal double-quotes themselves can be escaped by a backslash,
+ * but only when not within double quotes. Literal backslashes can
+ * be escaped by a backslash.
+ *
+ * Effectively, this means that instead of thinking about quoting
+ * individual strings, think about double quotes as an escaping
+ * mechanism for whitespace.
+ *
+ * In other words (using ` as a string boundary):
+ * [ `foo`, `bar` ] => `foo bar`
+ * [ `foo bar` ] => `foo" "bar`
+ * [ `foo bar`, `foo bar` ] => `foo" "bar foo" "bar`
+ * [ `foo "bar" foo` ] => `foo" "\"bar\"" "foo`
+ */
+int git_process__cmdline(
+ git_str *out,
+ const char **in,
+ size_t in_len)
+{
+ bool quoted = false;
+ const char *c;
+ size_t i;
+
+ for (i = 0; i < in_len; i++) {
+ /* Arguments are delimited by an unquoted space */
+ if (i)
+ git_str_putc(out, ' ');
+
+ for (c = in[i]; *c; c++) {
+ /* Start or stop quoting spaces within an argument */
+ if ((*c == ' ' || *c == '\t') && !quoted) {
+ git_str_putc(out, '"');
+ quoted = true;
+ } else if (*c != ' ' && *c != '\t' && quoted) {
+ git_str_putc(out, '"');
+ quoted = false;
+ }
+
+ /* Escape double-quotes and backslashes */
+ if (*c == '"' || *c == '\\')
+ git_str_putc(out, '\\');
+
+ git_str_putc(out, *c);
+ }
+ }
+
+ return git_str_oom(out) ? -1 : 0;
+}
+
+GIT_INLINE(bool) is_delete_env(const char *env)
+{
+ char *c = strchr(env, '=');
+
+ if (c == NULL)
+ return false;
+
+ return *(c+1) == '\0';
+}
+
+static int merge_env(wchar_t **out, const char **in, size_t in_len, bool exclude_env)
+{
+ git_str merged = GIT_BUF_INIT;
+ wchar_t *in16 = NULL, *env = NULL, *e;
+ char *e8 = NULL;
+ size_t e_len;
+ int ret = 0;
+ size_t i;
+
+ *out = NULL;
+
+ in16 = git__malloc(ENV_MAX * sizeof(wchar_t));
+ GIT_ERROR_CHECK_ALLOC(in16);
+
+ e8 = git__malloc(ENV_MAX);
+ GIT_ERROR_CHECK_ALLOC(e8);
+
+ for (i = 0; in && i < in_len; i++) {
+ if (is_delete_env(in[i]))
+ continue;
+
+ if ((ret = git_utf8_to_16(in16, ENV_MAX, in[i])) < 0)
+ goto done;
+
+ git_str_put(&merged, (const char *)in16, ret * 2);
+ git_str_put(&merged, "\0\0", 2);
+ }
+
+ if (!exclude_env) {
+ env = GetEnvironmentStringsW();
+
+ for (e = env; *e; e += (e_len + 1)) {
+ e_len = wcslen(e);
+
+ if ((ret = git_utf8_from_16(e8, ENV_MAX, e)) < 0)
+ goto done;
+
+ if (git_strlist_contains_key(in, in_len, e8, '='))
+ continue;
+
+ git_str_put(&merged, (const char *)e, e_len * 2);
+ git_str_put(&merged, "\0\0", 2);
+ }
+ }
+
+ git_str_put(&merged, "\0\0", 2);
+
+ *out = (wchar_t *)git_str_detach(&merged);
+
+done:
+ if (env)
+ FreeEnvironmentStringsW(env);
+
+ git_str_dispose(&merged);
+ git__free(e8);
+ git__free(in16);
+
+ return ret < 0 ? -1 : 0;
+}
+
+int git_process_new(
+ git_process **out,
+ const char **args,
+ size_t args_len,
+ const char **env,
+ size_t env_len,
+ git_process_options *opts)
+{
+ git_process *process;
+ git_str cmdline = GIT_BUF_INIT;
+ int error;
+
+ GIT_ASSERT_ARG(out && args && args_len > 0);
+
+ *out = NULL;
+
+ process = git__calloc(1, sizeof(git_process));
+ GIT_ERROR_CHECK_ALLOC(process);
+
+ if ((error = git_process__cmdline(&cmdline, args, args_len)) < 0)
+ goto done;
+
+ if (git_utf8_to_16_alloc(&process->appname, args[0]) < 0 ||
+ git_utf8_to_16_alloc(&process->cmdline, cmdline.ptr) < 0) {
+ error = -1;
+ goto done;
+ }
+
+ if (opts && opts->cwd &&
+ git_utf8_to_16_alloc(&process->cwd, opts->cwd) < 0) {
+ error = -1;
+ goto done;
+ }
+
+ if (env && (error = merge_env(&process->env, env, env_len, opts && opts->exclude_env) < 0))
+ goto done;
+
+ if (opts) {
+ process->capture_in = opts->capture_in;
+ process->capture_out = opts->capture_out;
+ process->capture_err = opts->capture_err;
+ }
+
+done:
+ if (error)
+ git_process_free(process);
+ else
+ *out = process;
+
+ git_str_dispose(&cmdline);
+ return error;
+}
+
+#define CLOSE_HANDLE(h) do { if ((h) != NULL) CloseHandle(h); } while(0)
+
+int git_process_start(git_process *process)
+{
+ STARTUPINFOW startup_info;
+ SECURITY_ATTRIBUTES security_attrs;
+ DWORD flags = CREATE_UNICODE_ENVIRONMENT;
+ HANDLE in[2] = { NULL, NULL },
+ out[2] = { NULL, NULL },
+ err[2] = { NULL, NULL };
+
+ memset(&security_attrs, 0, sizeof(SECURITY_ATTRIBUTES));
+ security_attrs.bInheritHandle = TRUE;
+
+ memset(&startup_info, 0, sizeof(STARTUPINFOW));
+ startup_info.cb = sizeof(STARTUPINFOW);
+ startup_info.hStdInput = GetStdHandle(STD_INPUT_HANDLE);
+ startup_info.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
+ startup_info.hStdError = GetStdHandle(STD_ERROR_HANDLE);
+
+ if (process->capture_in) {
+ if (!CreatePipe(&in[0], &in[1], &security_attrs, 0) ||
+ !SetHandleInformation(in[1], HANDLE_FLAG_INHERIT, 0)) {
+ git_error_set(GIT_ERROR_OS, "could not create pipe");
+ goto on_error;
+ }
+
+ startup_info.hStdInput = in[0];
+ startup_info.dwFlags |= STARTF_USESTDHANDLES;
+ }
+
+ if (process->capture_out) {
+ if (!CreatePipe(&out[0], &out[1], &security_attrs, 0) ||
+ !SetHandleInformation(out[0], HANDLE_FLAG_INHERIT, 0)) {
+ git_error_set(GIT_ERROR_OS, "could not create pipe");
+ goto on_error;
+ }
+
+ startup_info.hStdOutput = out[1];
+ startup_info.dwFlags |= STARTF_USESTDHANDLES;
+ }
+
+ if (process->capture_err) {
+ if (!CreatePipe(&err[0], &err[1], &security_attrs, 0) ||
+ !SetHandleInformation(err[0], HANDLE_FLAG_INHERIT, 0)) {
+ git_error_set(GIT_ERROR_OS, "could not create pipe");
+ goto on_error;
+ }
+
+ startup_info.hStdError = err[1];
+ startup_info.dwFlags |= STARTF_USESTDHANDLES;
+ }
+
+ memset(&process->process_info, 0, sizeof(PROCESS_INFORMATION));
+
+ if (!CreateProcessW(process->appname, process->cmdline,
+ NULL, NULL, TRUE, flags, process->env,
+ process->cwd,
+ &startup_info,
+ &process->process_info)) {
+ git_error_set(GIT_ERROR_OS, "could not create process");
+ goto on_error;
+ }
+
+ CLOSE_HANDLE(in[0]); process->child_in = in[1];
+ CLOSE_HANDLE(out[1]); process->child_out = out[0];
+ CLOSE_HANDLE(err[1]); process->child_err = err[0];
+
+ return 0;
+
+on_error:
+ CLOSE_HANDLE(in[0]); CLOSE_HANDLE(in[1]);
+ CLOSE_HANDLE(out[0]); CLOSE_HANDLE(out[1]);
+ CLOSE_HANDLE(err[0]); CLOSE_HANDLE(err[1]);
+ return -1;
+}
+
+ssize_t git_process_read(git_process *process, void *buf, size_t count)
+{
+ DWORD ret;
+
+ if (count > DWORD_MAX)
+ count = DWORD_MAX;
+ if (count > SSIZE_MAX)
+ count = SSIZE_MAX;
+
+ if (!ReadFile(process->child_out, buf, (DWORD)count, &ret, NULL)) {
+ if (GetLastError() == ERROR_BROKEN_PIPE)
+ return 0;
+
+ git_error_set(GIT_ERROR_OS, "could not read");
+ return -1;
+ }
+
+ return ret;
+}
+
+ssize_t git_process_write(git_process *process, const void *buf, size_t count)
+{
+ DWORD ret;
+
+ if (count > DWORD_MAX)
+ count = DWORD_MAX;
+ if (count > SSIZE_MAX)
+ count = SSIZE_MAX;
+
+ if (!WriteFile(process->child_in, buf, (DWORD)count, &ret, NULL)) {
+ git_error_set(GIT_ERROR_OS, "could not write");
+ return -1;
+ }
+
+ return ret;
+}
+
+int git_process_close_in(git_process *process)
+{
+ if (!process->capture_in) {
+ git_error_set(GIT_ERROR_INVALID, "input is not open");
+ return -1;
+ }
+
+ if (process->child_in) {
+ CloseHandle(process->child_in);
+ process->child_in = NULL;
+ }
+
+ return 0;
+}
+
+int git_process_close_out(git_process *process)
+{
+ if (!process->capture_out) {
+ git_error_set(GIT_ERROR_INVALID, "output is not open");
+ return -1;
+ }
+
+ if (process->child_out) {
+ CloseHandle(process->child_out);
+ process->child_out = NULL;
+ }
+
+ return 0;
+}
+
+int git_process_close_err(git_process *process)
+{
+ if (!process->capture_err) {
+ git_error_set(GIT_ERROR_INVALID, "error is not open");
+ return -1;
+ }
+
+ if (process->child_err) {
+ CloseHandle(process->child_err);
+ process->child_err = NULL;
+ }
+
+ return 0;
+}
+
+int git_process_close(git_process *process)
+{
+ if (process->child_in) {
+ CloseHandle(process->child_in);
+ process->child_in = NULL;
+ }
+
+ if (process->child_out) {
+ CloseHandle(process->child_out);
+ process->child_out = NULL;
+ }
+
+ if (process->child_err) {
+ CloseHandle(process->child_err);
+ process->child_err = NULL;
+ }
+
+ CloseHandle(process->process_info.hProcess);
+ process->process_info.hProcess = NULL;
+
+ CloseHandle(process->process_info.hThread);
+ process->process_info.hThread = NULL;
+
+ return 0;
+}
+
+int git_process_wait(git_process_result *result, git_process *process)
+{
+ DWORD exitcode;
+
+ if (result)
+ memset(result, 0, sizeof(git_process_result));
+
+ if (!process->process_info.dwProcessId) {
+ git_error_set(GIT_ERROR_INVALID, "process is stopped");
+ return -1;
+ }
+
+ if (WaitForSingleObject(process->process_info.hProcess, INFINITE) == WAIT_FAILED) {
+ git_error_set(GIT_ERROR_OS, "could not wait for process");
+ return -1;
+ }
+
+ if (!GetExitCodeProcess(process->process_info.hProcess, &exitcode)) {
+ git_error_set(GIT_ERROR_OS, "could not get process exit code");
+ return -1;
+ }
+
+ result->status = GIT_PROCESS_STATUS_NORMAL;
+ result->exitcode = exitcode;
+
+ memset(&process->process_info, 0, sizeof(PROCESS_INFORMATION));
+ return 0;
+}
+
+int git_process_result_msg(git_str *out, git_process_result *result)
+{
+ if (result->status == GIT_PROCESS_STATUS_NONE) {
+ return git_str_puts(out, "process not started");
+ } else if (result->status == GIT_PROCESS_STATUS_NORMAL) {
+ return git_str_printf(out, "process exited with code %d",
+ result->exitcode);
+ } else if (result->signal) {
+ return git_str_printf(out, "process exited on signal %d",
+ result->signal);
+ }
+
+ return git_str_puts(out, "unknown error");
+}
+
+void git_process_free(git_process *process)
+{
+ if (!process)
+ return;
+
+ if (process->process_info.hProcess)
+ git_process_close(process);
+
+ git__free(process->env);
+ git__free(process->cwd);
+ git__free(process->cmdline);
+ git__free(process->appname);
+ git__free(process);
+}
diff --git a/tests/resources/process/cat.bat b/tests/resources/process/cat.bat
new file mode 100644
index 000000000..af9b573c7
--- /dev/null
+++ b/tests/resources/process/cat.bat
@@ -0,0 +1,2 @@
+@ECHO OFF
+FOR /F "tokens=*" %%a IN ('more') DO ECHO %%a
diff --git a/tests/resources/process/env.cmd b/tests/resources/process/env.cmd
new file mode 100644
index 000000000..62675cf9e
--- /dev/null
+++ b/tests/resources/process/env.cmd
@@ -0,0 +1,2 @@
+@ECHO OFF
+SET
diff --git a/tests/resources/process/helloworld.sh b/tests/resources/process/helloworld.sh
new file mode 100755
index 000000000..0c4aefc38
--- /dev/null
+++ b/tests/resources/process/helloworld.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+echo "Hello, world."
diff --git a/tests/resources/process/pwd.bat b/tests/resources/process/pwd.bat
new file mode 100644
index 000000000..82e4fb60f
--- /dev/null
+++ b/tests/resources/process/pwd.bat
@@ -0,0 +1,2 @@
+@ECHO OFF
+ECHO %CD%
diff --git a/tests/util/process/env.c b/tests/util/process/env.c
new file mode 100644
index 000000000..d580d35e3
--- /dev/null
+++ b/tests/util/process/env.c
@@ -0,0 +1,111 @@
+#include "clar_libgit2.h"
+#include "process.h"
+#include "vector.h"
+
+static git_str env_cmd = GIT_BUF_INIT;
+static git_str accumulator = GIT_BUF_INIT;
+static git_vector env_result = GIT_VECTOR_INIT;
+
+void test_process_env__initialize(void)
+{
+#ifdef GIT_WIN32
+ git_str_printf(&env_cmd, "%s/env.cmd", cl_fixture("process"));
+#else
+ git_str_puts(&env_cmd, "/usr/bin/env");
+#endif
+
+ cl_git_pass(git_vector_init(&env_result, 32, git__strcmp_cb));
+}
+
+void test_process_env__cleanup(void)
+{
+ git_vector_free(&env_result);
+ git_str_dispose(&accumulator);
+ git_str_dispose(&env_cmd);
+}
+
+static void run_env(const char **env_array, size_t env_len, bool exclude_env)
+{
+ const char *args_array[] = { env_cmd.ptr };
+
+ git_process *process;
+ git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
+ git_process_result result = GIT_PROCESS_RESULT_INIT;
+
+ char buf[1024], *tok;
+ ssize_t ret;
+
+ opts.capture_out = 1;
+ opts.exclude_env = exclude_env;
+
+ cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), env_array, env_len, &opts));
+ cl_git_pass(git_process_start(process));
+
+ while ((ret = git_process_read(process, buf, 1024)) > 0)
+ cl_git_pass(git_str_put(&accumulator, buf, (size_t)ret));
+
+ cl_assert_equal_i(0, ret);
+
+ cl_git_pass(git_process_wait(&result, process));
+
+ cl_assert_equal_i(GIT_PROCESS_STATUS_NORMAL, result.status);
+ cl_assert_equal_i(0, result.exitcode);
+ cl_assert_equal_i(0, result.signal);
+
+ for (tok = strtok(accumulator.ptr, "\n"); tok; tok = strtok(NULL, "\n")) {
+#ifdef GIT_WIN32
+ if (strlen(tok) && tok[strlen(tok) - 1] == '\r')
+ tok[strlen(tok) - 1] = '\0';
+#endif
+
+ cl_git_pass(git_vector_insert(&env_result, tok));
+ }
+
+ git_process_close(process);
+ git_process_free(process);
+}
+
+void test_process_env__can_add_env(void)
+{
+ const char *env_array[] = { "TEST_NEW_ENV=added", "TEST_OTHER_ENV=also_added" };
+ run_env(env_array, 2, false);
+
+ cl_git_pass(git_vector_search(NULL, &env_result, "TEST_NEW_ENV=added"));
+ cl_git_pass(git_vector_search(NULL, &env_result, "TEST_OTHER_ENV=also_added"));
+}
+
+void test_process_env__can_propagate_env(void)
+{
+ cl_setenv("TEST_NEW_ENV", "propagated");
+ run_env(NULL, 0, false);
+
+ cl_git_pass(git_vector_search(NULL, &env_result, "TEST_NEW_ENV=propagated"));
+}
+
+void test_process_env__can_remove_env(void)
+{
+ const char *env_array[] = { "TEST_NEW_ENV=" };
+ char *str;
+ size_t i;
+
+ cl_setenv("TEST_NEW_ENV", "propagated");
+ run_env(env_array, 1, false);
+
+ git_vector_foreach(&env_result, i, str)
+ cl_assert(git__prefixcmp(str, "TEST_NEW_ENV=") != 0);
+}
+
+void test_process_env__can_clear_env(void)
+{
+ const char *env_array[] = { "TEST_NEW_ENV=added", "TEST_OTHER_ENV=also_added" };
+
+ cl_setenv("SOME_EXISTING_ENV", "propagated");
+ run_env(env_array, 2, true);
+
+ /*
+ * We can't simply test that the environment is precisely what we
+ * provided. Some systems (eg win32) will add environment variables
+ * to all processes.
+ */
+ cl_assert_equal_i(GIT_ENOTFOUND, git_vector_search(NULL, &env_result, "SOME_EXISTING_ENV=propagated"));
+}
diff --git a/tests/util/process/start.c b/tests/util/process/start.c
new file mode 100644
index 000000000..cb43bf746
--- /dev/null
+++ b/tests/util/process/start.c
@@ -0,0 +1,212 @@
+#include "clar_libgit2.h"
+#include "process.h"
+#include "vector.h"
+
+#ifndef SIGPIPE
+# define SIGPIPE 42
+#endif
+
+static git_str helloworld_cmd = GIT_BUF_INIT;
+static git_str cat_cmd = GIT_BUF_INIT;
+static git_str pwd_cmd = GIT_BUF_INIT;
+
+void test_process_start__initialize(void)
+{
+#ifdef GIT_WIN32
+ git_str_printf(&helloworld_cmd, "%s/helloworld.bat", cl_fixture("process"));
+ git_str_printf(&cat_cmd, "%s/cat.bat", cl_fixture("process"));
+ git_str_printf(&pwd_cmd, "%s/pwd.bat", cl_fixture("process"));
+#else
+ git_str_printf(&helloworld_cmd, "%s/helloworld.sh", cl_fixture("process"));
+#endif
+}
+
+void test_process_start__cleanup(void)
+{
+ git_str_dispose(&pwd_cmd);
+ git_str_dispose(&cat_cmd);
+ git_str_dispose(&helloworld_cmd);
+}
+
+void test_process_start__returncode(void)
+{
+#ifdef GIT_WIN32
+ const char *args_array[] = { "C:\\Windows\\System32\\cmd.exe", "/c", "exit", "1" };
+#elif __APPLE__
+ const char *args_array[] = { "/usr/bin/false" };
+#else
+ const char *args_array[] = { "/bin/false" };
+#endif
+
+ git_process *process;
+ git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
+ git_process_result result = GIT_PROCESS_RESULT_INIT;
+
+ cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), NULL, 0, &opts));
+ cl_git_pass(git_process_start(process));
+ cl_git_pass(git_process_wait(&result, process));
+
+ cl_assert_equal_i(GIT_PROCESS_STATUS_NORMAL, result.status);
+ cl_assert_equal_i(1, result.exitcode);
+ cl_assert_equal_i(0, result.signal);
+
+ git_process_free(process);
+}
+
+void test_process_start__not_found(void)
+{
+#ifdef GIT_WIN32
+ const char *args_array[] = { "C:\\a\\b\\z\\y\\not_found" };
+#else
+ const char *args_array[] = { "/a/b/z/y/not_found" };
+#endif
+
+ git_process *process;
+ git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
+
+ cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), NULL, 0, &opts));
+ cl_git_fail(git_process_start(process));
+ git_process_free(process);
+}
+
+static void write_all(git_process *process, char *buf)
+{
+ size_t buf_len = strlen(buf);
+ ssize_t ret;
+
+ while (buf_len) {
+ ret = git_process_write(process, buf, buf_len);
+ cl_git_pass(ret < 0 ? (int)ret : 0);
+
+ buf += ret;
+ buf_len -= ret;
+ }
+}
+
+static void read_all(git_str *out, git_process *process)
+{
+ char buf[32];
+ size_t buf_len = 32;
+ ssize_t ret;
+
+ while ((ret = git_process_read(process, buf, buf_len)) > 0)
+ cl_git_pass(git_str_put(out, buf, ret));
+
+ cl_git_pass(ret < 0 ? (int)ret : 0);
+}
+
+void test_process_start__redirect_stdio(void)
+{
+#ifdef GIT_WIN32
+ const char *args_array[] = { "C:\\Windows\\System32\\cmd.exe", "/c", cat_cmd.ptr };
+#else
+ const char *args_array[] = { "/bin/cat" };
+#endif
+
+ git_process *process;
+ git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
+ git_process_result result = GIT_PROCESS_RESULT_INIT;
+ git_str buf = GIT_BUF_INIT;
+
+ opts.capture_in = 1;
+ opts.capture_out = 1;
+
+ cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), NULL, 0, &opts));
+ cl_git_pass(git_process_start(process));
+
+ write_all(process, "Hello, world.\r\nHello!\r\n");
+ cl_git_pass(git_process_close_in(process));
+
+ read_all(&buf, process);
+ cl_assert_equal_s("Hello, world.\r\nHello!\r\n", buf.ptr);
+
+ cl_git_pass(git_process_wait(&result, process));
+
+ cl_assert_equal_i(GIT_PROCESS_STATUS_NORMAL, result.status);
+ cl_assert_equal_i(0, result.exitcode);
+ cl_assert_equal_i(0, result.signal);
+
+ git_str_dispose(&buf);
+ git_process_free(process);
+}
+
+void test_process_start__catch_signal(void)
+{
+#ifndef GIT_WIN32
+ const char *args_array[] = { helloworld_cmd.ptr };
+
+ git_process *process;
+ git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
+ git_process_result result = GIT_PROCESS_RESULT_INIT;
+
+ opts.capture_out = 1;
+
+ cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), NULL, 0, &opts));
+ cl_git_pass(git_process_start(process));
+ cl_git_pass(git_process_close(process));
+ cl_git_pass(git_process_wait(&result, process));
+
+ cl_assert_equal_i(GIT_PROCESS_STATUS_ERROR, result.status);
+ cl_assert_equal_i(0, result.exitcode);
+ cl_assert_equal_i(SIGPIPE, result.signal);
+
+ git_process_free(process);
+#endif
+}
+
+void test_process_start__can_chdir(void)
+{
+#ifdef GIT_WIN32
+ const char *args_array[] = { "C:\\Windows\\System32\\cmd.exe", "/c", pwd_cmd.ptr };
+ char *startwd = "C:\\";
+#else
+ const char *args_array[] = { "/bin/pwd" };
+ char *startwd = "/";
+#endif
+
+ git_process *process;
+ git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
+ git_process_result result = GIT_PROCESS_RESULT_INIT;
+ git_str buf = GIT_BUF_INIT;
+
+ opts.cwd = startwd;
+ opts.capture_out = 1;
+
+ cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), NULL, 0, &opts));
+ cl_git_pass(git_process_start(process));
+
+ read_all(&buf, process);
+ git_str_rtrim(&buf);
+
+ cl_assert_equal_s(startwd, buf.ptr);
+
+ cl_git_pass(git_process_wait(&result, process));
+
+ cl_assert_equal_i(GIT_PROCESS_STATUS_NORMAL, result.status);
+ cl_assert_equal_i(0, result.exitcode);
+ cl_assert_equal_i(0, result.signal);
+
+ git_str_dispose(&buf);
+ git_process_free(process);
+}
+
+void test_process_start__cannot_chdir_to_nonexistent_dir(void)
+{
+#ifdef GIT_WIN32
+ const char *args_array[] = { "C:\\Windows\\System32\\cmd.exe", "/c", pwd_cmd.ptr };
+ char *startwd = "C:\\a\\b\\z\\y\\not_found";
+#else
+ const char *args_array[] = { "/bin/pwd" };
+ char *startwd = "/a/b/z/y/not_found";
+#endif
+
+ git_process *process;
+ git_process_options opts = GIT_PROCESS_OPTIONS_INIT;
+
+ opts.cwd = startwd;
+ opts.capture_out = 1;
+
+ cl_git_pass(git_process_new(&process, args_array, ARRAY_SIZE(args_array), NULL, 0, &opts));
+ cl_git_fail(git_process_start(process));
+ git_process_free(process);
+}
diff --git a/tests/util/process/win32.c b/tests/util/process/win32.c
new file mode 100644
index 000000000..7442ee089
--- /dev/null
+++ b/tests/util/process/win32.c
@@ -0,0 +1,64 @@
+#include "clar_libgit2.h"
+#include "process.h"
+#include "vector.h"
+
+#ifdef GIT_WIN32
+static git_strarray input;
+static git_str result;
+
+# define assert_cmdline(expected, given) do { \
+ input.count = ARRAY_SIZE(given); \
+ input.strings = given; \
+ cl_git_pass(git_process__cmdline(&result, &input)); \
+ cl_assert_equal_s(expected, result.ptr); \
+ git_str_dispose(&result); \
+ } while(0)
+#endif
+
+void test_process_win32__cmdline_is_whitespace_delimited(void)
+{
+#ifdef GIT_WIN32
+ char *one[] = { "one" };
+ char *two[] = { "one", "two" };
+ char *three[] = { "one", "two", "three" };
+ char *four[] = { "one", "two", "three", "four" };
+
+ assert_cmdline("one", one);
+ assert_cmdline("one two", two);
+ assert_cmdline("one two three", three);
+ assert_cmdline("one two three four", four);
+#endif
+}
+
+void test_process_win32__cmdline_escapes_whitespace(void)
+{
+#ifdef GIT_WIN32
+ char *spaces[] = { "one with spaces" };
+ char *tabs[] = { "one\twith\ttabs" };
+ char *multiple[] = { "one with many spaces" };
+
+ assert_cmdline("one\" \"with\" \"spaces", spaces);
+ assert_cmdline("one\"\t\"with\"\t\"tabs", tabs);
+ assert_cmdline("one\" \"with\" \"many\" \"spaces", multiple);
+#endif
+}
+
+void test_process_win32__cmdline_escapes_quotes(void)
+{
+#ifdef GIT_WIN32
+ char *one[] = { "echo", "\"hello world\"" };
+
+ assert_cmdline("echo \\\"hello\" \"world\\\"", one);
+#endif
+}
+
+void test_process_win32__cmdline_escapes_backslash(void)
+{
+#ifdef GIT_WIN32
+ char *one[] = { "foo\\bar", "foo\\baz" };
+ char *two[] = { "c:\\program files\\foo bar\\foo bar.exe", "c:\\path\\to\\other\\", "/a", "/b" };
+
+ assert_cmdline("foo\\\\bar foo\\\\baz", one);
+ assert_cmdline("c:\\\\program\" \"files\\\\foo\" \"bar\\\\foo\" \"bar.exe c:\\\\path\\\\to\\\\other\\\\ /a /b", two);
+#endif
+}