Merge branch 'pipeexec'
authorThomas Jarosch <thomas.jarosch@intra2net.com>
Tue, 14 Aug 2018 15:24:14 +0000 (17:24 +0200)
committerThomas Jarosch <thomas.jarosch@intra2net.com>
Tue, 14 Aug 2018 15:24:14 +0000 (17:24 +0200)
Thanks Philipp!

CMakeLists.txt
src/pipestream.cpp
src/pipestream.hxx
test/CMakeLists.txt
test/test_pipestream.cpp [new file with mode: 0644]

index 3979476..18d53b6 100644 (file)
@@ -2,7 +2,7 @@
 project(libi2ncommon)
 
 # Version settings
-set(VERSION 2.9)
+set(VERSION 2.10)
 
 SET(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}")
 
index afa6d56..a5e4a00 100644 (file)
@@ -24,24 +24,33 @@ on this file might be covered by the GNU General Public License.
     copyright            : (C) 2001 by Intra2net AG
  ***************************************************************************/
 
+#include <errno.h>
 #include <stdio.h>
+#include <string.h>
+#include <fcntl.h>
 #include <sys/wait.h>
+#include <unistd.h>
 
-#include <string>
 #include <streambuf>
 #include <istream>
 #include <ostream>
 #include <cstdio>
+#include <boost/foreach.hpp>
+#include <boost/shared_array.hpp>
 
 #include "exception.hxx"
+#include "stringfunc.hxx"
 #include "pipestream.hxx"
 
 /** @brief runs command and returns it's output as string
  *  @param command the full command with all parameters
  *  @param rescode struct containing the return code, if the program exited normally and so on
+ *  @param flags runtime control flags (stdio streams, environment, path lookup).
  *  @returns the output (stdout) of the called program
  */
-std::string capture_exec(const std::string& command, ExecResult &rescode)
+template <typename CmdT>
+std::string capture_exec(CmdT command, ExecResult &rescode,
+                         const int flags)
 {
     std::string output;
 
@@ -55,7 +64,7 @@ std::string capture_exec(const std::string& command, ExecResult &rescode)
     try
     {
         {
-            inpipestream ips(command);
+            inpipestream ips(command, flags);
 
             ips.store_exit_status(&exit_set, &exit_status_waitpid);
 
@@ -88,11 +97,404 @@ std::string capture_exec(const std::string& command, ExecResult &rescode)
     return output;
 }
 
-inpipebuf::inpipebuf(const std::string& command)
+/** @brief      Instantiation of \c capture_exec for STL string arguments.
+ *              Caveat emptor: this will cause the backing stream to use \c
+ *              popen(3). To avoid shelling out, please refer to one of the
+ *              variants that allow passing an argument list.
+ *
+ *  @param command    String specifying the shell expression to be executed.
+ *  @param res        (Out parameter) Store information about the termination
+ *                    state in this struct.
+ *
+ *  @returns          Result of \c stdout. Note that due to the use of \c
+ *                    popen, the correct way to collect stderr output as
+ *                    well is to use shell redirection inside the expression
+ *                    passed.
+ */
+std::string capture_exec (const std::string &command, ExecResult &res)
+{ return capture_exec<const std::string &>(command, res, capture_flag::dflt); }
+
+/** @brief      Instantiation of \c capture_exec for argument lists. The
+ *              pipestream used to run the command will not shell out.
+ *              One of \c out or \c err must be set.
+ *
+ *  @param command    List of \c char* specifying the \c argv array of the
+ *                    command to run. Note that the binary to executed is
+ *                    assumed to be present at index 0 and that the input
+ *                    is properly \c NULL terminated.
+ *  @param res        (Out parameter) Store information about the termination
+ *                    state in this struct.
+ *  @param flags      Runtime control flags (stdio streams, environment, path
+ *                    lookup).
+ *
+ *  @returns          Captured output, combined into one string.
+ */
+std::string capture_exec (const char *const *command, ExecResult &res,
+                          const int flags)
+{ return capture_exec<const char *const *>(command, res, flags); }
+
+/** @brief      Instantiation of \c capture_exec for argument lists. The
+ *              pipestream used to run the command will not shell out.
+ *              One of \c out or \c err must be set.
+ *
+ *  @param command    String vector specifying the \c argv array of the
+ *                    command to run. Note that the binary to executed is
+ *                    assumed to be present at index 0.
+ *  @param res        (Out parameter) Store information about the termination
+ *                    state in this struct.
+ *  @param flags      Runtime control flags (stdio streams, environment, path
+ *                    lookup).
+ *
+ *  @returns          Captured output, combined into one string.
+ */
+std::string capture_exec (const std::vector<std::string> &command, ExecResult &res,
+                          const int flags)
+{ return capture_exec<const std::vector<std::string> &> (command, res, flags); }
+
+#define PIPE_CTOR_FAIL(where) \
+    do { \
+        throw EXCEPTION (pipestream_error, \
+                         std::string (where) + ": error " \
+                         + I2n::to_string (errno) \
+                         + " (" + std::string (strerror (errno)) + ")"); \
+    } while (0)
+
+/** @brief      Convert a string vector to a refcounted \c char**
+ *              that is \c NULL terminated for use with e. g. \c execve(2).
+ *
+ *  @param command    List of arguments including the binary at index 0.
+ *
+ *  @returns          A \c boost::shared_array of pointers to the
+ *                    arguments plus a trailing \c NULL. Note that
+ *                    while the array itself is refcounted, the
+ *                    pointees are assumed owned by the caller and
+ *                    *not copyied*. I. e. they lose validity if the
+ *                    original strings are freed.
+ */
+static boost::shared_array <char *>
+mk_argv (const std::vector<std::string> &command)
 {
-    status_set = NULL;
-    exit_status = NULL;
+    char **ret = NULL;
+
+    try {
+        ret = new char *[command.size () * sizeof (ret[0]) + 1];
+    } catch (std::bad_alloc &) {
+        return boost::shared_array<char *> ();
+    }
+
+    size_t cur = 0;
+    BOOST_FOREACH(const std::string &arg, command) {
+        /*
+         * Casting away constness is safe since the data is always
+         * kept alive until after exec().
+         */
+        ret [cur++] = const_cast<char *> (arg.c_str ());
+    }
+
+    ret [cur] = NULL;
+
+    return boost::shared_array<char *> (ret);
+}
+
+/** @brief      Helper for redirecting a file descriptor to \c /dev/null.
+ *              This will only acquire an fd the first time it is called
+ *              or if it is called after unsuccessfully attempting to
+ *              acquire one.
+ *
+ *  @param fd         The open file descriptor to operate on.
+ *  @param save_errno Out parameter: stores errno here after a syscall failure.
+ *
+ *  @returns          \c true on success, \c false otherwise (the call to
+ *                    either \c open(2) or \c dup2(2) failed), with errno
+ *                    communicated through saved_errno.
+ */
+static bool
+redirect_devnull (const int fd, int &save_errno)
+{
+    static int nullfd = -1;
+    
+    errno = 0;
+    if (nullfd == -1 && (nullfd = open ("/dev/null", O_RDWR)) == -1) {
+        save_errno = errno;
+        return false;
+    }
+
+    errno = 0;
+    if (dup2 (nullfd, fd) == -1) {
+        save_errno = errno;
+        return false;
+    }
+
+    return true;
+}
+
+/** @brief      Helper aggregating common code for the shell-free ctors.
+ *
+ *  @param argv       Argument list prepared for \c execve(2).
+ *  @param flags      Control the runtime behavior wrt. stdio streams, \c
+ *                    *envp, and path search. One of \c collect_out or
+ *                    \c collect_err is mandatory. All other flags are
+ *                    optional. Pipebuf creation with fail with \c EINVAL
+ *                    if that constraint is violated.
+ *
+ *  @returns          A \c FILE* handle for streaming if successful, \c NULL
+ *                    otherwise.
+ *
+ * Error handling strategy:
+ *
+ *      - receive all errors from child as ints through a cloexec pipe;
+ *      - in the child, write error conditions always to pipe first,
+ *        then try to emit a more verbose log message;
+ *      - in the parent, throw on error indicating the child errno.
+ *
+ * Note that the error-pipe approach is robust due to guarantees by both
+ * standard (POSIX) and implementation (Linux) of pipes: The read(2) from
+ * the error channel will block until the pipe is either closed or written to;
+ * hence no need to check for EAGAIN. Those writes are guaranteed to be atomic
+ * because sizeof(errno) is less than PIPE_BUF; hence we can disregard EINTR. A
+ * pipe whose write end (i.e. in the child) has been closed (by the kernel
+ * because execve(2) was successful) will always indicate EOF by returning
+ * zero, hence we know precisely whether everything went well or not. Cf.
+ * pipe(7), sections “I/O on pipes and FIFOs” and “PIPE_BUF”, as well as
+ * Kerrisk (2010), section 44.10, p. 917f.
+ */
+std::pair <pid_t, FILE *>
+inpipebuf::init_without_shell (const char *const *argv,
+                               const int flags) const
+{
+    FILE *pipeobj = NULL;
+    int pipefd [2]; /* for reading output from the child */
+    int errfd  [2]; /* for determining a successful exec() */
+    sigset_t oldmask, newmask;
+    char *const *envp = flags & capture_flag::env_passthru ? environ : NULL;
+
+    if (!(flags & capture_flag::collect_any))
+    {
+        errno = EINVAL;
+        PIPE_CTOR_FAIL("ctor");
+    }
+
+    /*
+     * The error pipe must be openend with *O_CLOEXEC* set. We also open
+     * the data pipe with close-on-exec and remove that bit only in the child.
+     * The rationale is preventing the read fd from passed on if the parent
+     * later re-forks another child: we intend it to be read from this (master)
+     * process alone.
+     */
+    errno = 0;
+    if (   ::pipe2 (pipefd, O_CLOEXEC) == -1
+        || ::pipe2 (errfd , O_CLOEXEC) == -1) {
+        PIPE_CTOR_FAIL("pipe2");
+    }
+
+    sigfillset (&newmask);
+    sigprocmask (SIG_SETMASK, &newmask, &oldmask);
+
+    errno = 0;
+    pid_t childpid = fork ();
+    switch (childpid) {
+        case -1: {
+            sigprocmask (SIG_SETMASK, &oldmask, NULL);
+            PIPE_CTOR_FAIL("fork");
+            break;
+        }
+        case 0: {
+            /*
+             * Close read ends of error and data channels: the child is assumed
+             * to write exclusively.
+             */
+            close (pipefd [0]);
+            close (errfd  [0]);
+
+            /*
+             * Remove cloexec bit from the write end of the pipe (this is the
+             * only flag with F_SETFD).
+             */
+            fcntl (pipefd [1], F_SETFD, 0);
+
+            /*
+             * Prevent the child from receiving more privileges than the
+             * parent. This concerns mainly suid binaries.
+             */
+            errno = 0;
+            if (   flags & capture_flag::no_new_privs
+                && prctl (PR_SET_NO_NEW_PRIVS,  1, 0, 0, 0) == -1)
+            {
+                (void)write (errfd [1], (char *)&errno, sizeof(errno));
+                exit (EXIT_FAILURE);
+            }
+
+            int save_errno = 0;
+
+            /*
+             * Assign /dev/null if asked to close one of the streams, else
+             * dup() it onto the pipe.
+             */
+            if (!(flags & capture_flag::collect_out))
+            {
+                if (!redirect_devnull (STDOUT_FILENO, save_errno))
+                {
+                    (void)write (errfd [1], (char *)&save_errno, sizeof(save_errno));
+                    exit (EXIT_FAILURE);
+                }
+            }
+            else if (dup2 (pipefd[1], STDOUT_FILENO) == -1)
+            {
+                (void)write (errfd [1], (char *)&save_errno, sizeof(save_errno));
+                exit (EXIT_FAILURE);
+            }
+
+            if (!(flags & capture_flag::collect_err))
+            {
+                if (!redirect_devnull (STDERR_FILENO, save_errno))
+                {
+                    (void)write (errfd [1], (char *)&save_errno, sizeof(save_errno));
+                    exit (EXIT_FAILURE);
+                }
+            }
+            else if (dup2 (pipefd[1], STDERR_FILENO) == -1)
+            {
+                (void)write (errfd [1], (char *)&save_errno, sizeof(save_errno));
+                exit (EXIT_FAILURE);
+            }
 
+            /*
+             * Close the write end of the pipe now that we have dup()’ed it
+             * onto the stdio fds. The parent will now receive EOF on the pipe
+             * when these fds are both closed.
+             */
+            close (pipefd [1]);
+
+            /*
+             * Stop blocking signals so the child starts out with a sane
+             * environment.
+             */
+            sigprocmask (SIG_SETMASK, &oldmask, NULL);
+
+            errno = 0;
+            if (flags & capture_flag::search_path) {
+                execvpe (argv [0], const_cast <char *const *>(argv), envp);
+            } else {
+                execve (argv [0], const_cast <char *const *>(argv), envp);
+            }
+
+            /*
+             * At this point, the call to execv[p]e() failed. Thus the error
+             * pipe is still opened and we forward the errno through it.
+             */
+            (void)write (errfd [1], (char *)&errno, sizeof(errno));
+            exit (EXIT_FAILURE);
+            break;
+        }
+        default: {
+            break;
+        }
+    }
+
+    /*
+     * The parent is assumed to only consume data from either pipe, never
+     * write.
+     */
+    close (pipefd [1]);
+    close (errfd  [1]);
+
+    /*
+     * Check whether the child exec()’ed by reading from the error pipe.
+     * The call to read(2) will block, uninterruptible due to signals being
+     * blocked. If all went well, the read(2) will return zero bytes and we can
+     * ditch the error channel.
+     *
+     * Otherwise either the read(2) failed or we actually received something
+     * through the error pipe. Both cases are treated as errors and cause an
+     * exit from the ctor.
+     */
+    char buf [sizeof (errno)];
+    int ret;
+    memset (buf, 0, sizeof (buf));
+    errno = 0;
+    if ((ret = read (errfd [0], buf, sizeof (buf))) != 0) {
+        close (pipefd [0]);
+        close (errfd  [0]);
+        sigprocmask (SIG_SETMASK, &oldmask, NULL);
+        if (ret == - 1) {
+            /* read(2) failed */
+            PIPE_CTOR_FAIL("read");
+        } else {
+            /*
+             * We received data on the error channel indicating the child
+             * process never successfully exec()’ed. We grab the error code
+             * from the buffer and bail.
+             */
+            errno = *((int *)&buf[0]);
+            PIPE_CTOR_FAIL("child failed to exec()");
+        }
+    }
+
+    /*
+     * read(2) yielded zero bytes; it’s safe to use the pipe so close our end
+     * and continue.
+     */
+    close (errfd [0]);
+
+    sigprocmask (SIG_SETMASK, &oldmask, NULL);
+
+    errno = 0;
+    if ((pipeobj = fdopen (pipefd [0], "r")) == NULL) {
+        close (pipefd [0]);
+        PIPE_CTOR_FAIL("fdopen");
+    }
+
+    return std::make_pair (childpid, pipeobj);
+}
+
+inpipebuf::inpipebuf(const char *const *command,
+                     const int flags)
+    : pipe (NULL) /* brr: shadowing global ident */
+    , pid (-1)
+    , status_set (NULL)
+    , exit_status (NULL)
+{
+    if (command == NULL || command [0] == NULL) {
+        PIPE_CTOR_FAIL("command");
+    }
+
+    std::pair <pid_t, FILE *> tmp = this->init_without_shell (command, flags);
+    this->pid  = tmp.first; /* no std::tie :/ */
+    this->pipe = tmp.second;
+
+    setg (&buffer, &buffer, &buffer);
+}
+
+inpipebuf::inpipebuf(const std::vector<std::string> &command,
+                     const int flags)
+    : pipe (NULL) /* brr: shadowing global ident */
+    , pid (-1)
+    , status_set (NULL)
+    , exit_status (NULL)
+{
+    if (command.empty ()) {
+        PIPE_CTOR_FAIL("command");
+    }
+
+    const boost::shared_array <char *> argv = mk_argv (command);
+    if (!argv) {
+        PIPE_CTOR_FAIL("malloc");
+    }
+
+    std::pair <pid_t, FILE *> tmp =
+        this->init_without_shell (argv.get (), flags);
+    this->pid  = tmp.first;
+    this->pipe = tmp.second;
+
+    setg (&buffer, &buffer, &buffer);
+}
+
+inpipebuf::inpipebuf(const std::string& command,
+                     const int _ignored_flags)
+    : pid (-1)
+    , status_set (NULL)
+    , exit_status (NULL)
+{
     pipe = popen (command.c_str(), "r");
     if (pipe == NULL)
         throw EXCEPTION (pipestream_error, "can't open program or permission denied");
@@ -104,13 +506,47 @@ inpipebuf::inpipebuf(const std::string& command)
 inpipebuf::~inpipebuf()
 {
     if (pipe != NULL) {
-        int pclose_exit = pclose (pipe);
+        int status;
 
-        if (exit_status && pclose_exit != -1)
+        if (this->pid == -1)
         {
-            if (status_set)
-                *status_set = true;
-            *exit_status = pclose_exit;
+            errno = 0;
+            status = pclose (pipe);
+            if (status != -1) {
+                if (exit_status != NULL) {
+                    *exit_status = status;
+                    if (status_set != NULL) {
+                        *status_set = true;
+                    }
+                }
+            }
+        }
+        else
+        {
+            errno = 0;
+            status = fclose (pipe);
+            if (status != EOF) {
+                if (exit_status != NULL) {
+                    *exit_status = status; /* might be overwritten below */
+                    if (status_set != NULL) {
+                        *status_set = true;
+                    }
+                }
+            }
+
+            errno = 0;
+            while (waitpid (this->pid, &status, 0) == -1) {
+                if (errno != EINTR) {
+                    status = -1;
+                    break;
+                }
+            }
+            if (status != 0 && exit_status != NULL) {
+                *exit_status = status; /* might overwrite pipe status above */
+                if (status_set != NULL) {
+                    *status_set = true;
+                }
+            }
         }
 
         pipe = NULL;
index 9280859..e22479b 100644 (file)
@@ -27,12 +27,17 @@ on this file might be covered by the GNU General Public License.
 #ifndef _PIPESTREAM
 #define _PIPESTREAM
 
+#include <sys/prctl.h>
 #include <stdio.h>
 
+#include <cstring>
 #include <string>
 #include <streambuf>
 #include <istream>
 #include <ostream>
+#include <vector>
+
+#include <stringfunc.hxx>
 
 struct ExecResult
 {
@@ -50,12 +55,53 @@ struct ExecResult
 
     /** errormessage if we have one */
     std::string error_message;
+
+    inline std::string format (void) const
+    {
+        return std::string ("(")
+            + "(normal_exit " + (this->normal_exit ? "T" : "F") + ") "
+              "(return_code '" + I2n::to_string ((int)this->return_code) + "') "
+              "(signal " + (this->terminated_by_signal
+                            ? strsignal (this->signal)
+                            : "<nil>") + "))"
+            ;
+    };
 };
 typedef struct ExecResult ExecResult;
 
+namespace capture_flag {
+
+    static const int none           =      0;
+    static const int collect_out    = 1 << 0;
+    static const int collect_err    = 1 << 1;
+    static const int search_path    = 1 << 2;
+    static const int env_passthru   = 1 << 3;
+    static const int no_new_privs   = 1 << 4;
+
+    static const int dflt           = collect_out;
+    static const int collect_any    = collect_out | collect_err;
+
+} /* [namespace capture_flag] */
+
 std::string capture_exec(const std::string& command, ExecResult &rescode);
+std::string capture_exec(const char *const *command, ExecResult &rescode,
+                         const int flags=capture_flag::dflt);
+std::string capture_exec(const std::vector<std::string>& command, ExecResult &rescode,
+                         const int flags=capture_flag::dflt);
+
+inline std::string capture_exec (const std::string &command)
+{
+    ExecResult r;
+    return capture_exec(command,r);
+}
 
-inline std::string capture_exec(const std::string& command)
+inline std::string capture_exec(const char *const *command)
+{
+    ExecResult r;
+    return capture_exec(command,r);
+}
+
+inline std::string capture_exec(const std::vector<std::string>& command)
 {
     ExecResult r;
     return capture_exec(command,r);
@@ -74,14 +120,18 @@ class inpipebuf : public std::streambuf
 {
 protected:
     char buffer;
+
     FILE *pipe;
+    pid_t pid;
 
     // "callback" variables for destructor to store exit status
     bool *status_set;
     int *exit_status;
 
 public:
-    inpipebuf(const std::string& command);
+    inpipebuf(const std::string& command, const int flags);
+    inpipebuf(const char *const *command, const int flags);
+    inpipebuf(const std::vector<std::string> &command, const int flags);
 
     ~inpipebuf();
 
@@ -89,6 +139,10 @@ public:
 
 protected:
     virtual int_type underflow();
+
+private:
+    std::pair <pid_t, FILE *>
+    init_without_shell (const char *const *argv, const int flags) const;
 };
 
 /** @brief stream around inpipebuf -- see comment there */
@@ -98,8 +152,19 @@ protected:
     inpipebuf buf;
 
 public:
-    inpipestream(const std::string& command)
-            : std::istream(&buf), buf(command)
+    inpipestream(const std::string& command,
+                 const int flags=capture_flag::dflt)
+            : std::istream(&buf), buf(command, flags)
+    {}
+
+    inpipestream(const char *const command[],
+                 const int flags=capture_flag::dflt)
+            : std::istream(&buf), buf(command, flags)
+    {}
+
+    inpipestream(const std::vector<std::string> &command,
+                 const int flags=capture_flag::dflt)
+            : std::istream(&buf), buf(command, flags)
     {}
 
     void store_exit_status(bool *_status_set, int *_exit_status)
index 2928716..ecd259b 100644 (file)
@@ -17,6 +17,7 @@ SET(cpp_sources
    test_logging.cpp
    test_pidfile.cpp
    test_restricted_html.cpp
+   test_pipestream.cpp
    test_timefunc.cpp
    test_tmpfstream.cpp
    test_tribool.cpp
diff --git a/test/test_pipestream.cpp b/test/test_pipestream.cpp
new file mode 100644 (file)
index 0000000..16e7635
--- /dev/null
@@ -0,0 +1,424 @@
+/*
+ *  The software in this package is distributed under the GNU General
+ *  Public License version 2 (with a special exception described below).
+ *
+ *  A copy of GNU General Public License (GPL) is included in this distribution,
+ *  in the file COPYING.GPL.
+ *
+ *  As a special exception, if other files instantiate templates or use macros
+ *  or inline functions from this file, or you compile this file and link it
+ *  with other works to produce a work based on this file, this file
+ *  does not by itself cause the resulting work to be covered
+ *  by the GNU General Public License.
+ *
+ *  However the source code for this file must still be made available
+ *  in accordance with section (3) of the GNU General Public License.
+ *
+ *  This exception does not invalidate any other reasons why a work based
+ *  on this file might be covered by the GNU General Public License.
+ *
+ * @file
+ *
+ * unit tests for the module "pipestream"
+ *
+ * Copyright 2018 by Intra2net AG
+ */
+
+#include <boost/version.hpp>
+#if BOOST_VERSION > /* guessed */ 104400
+/*
+ * Boost overeagerly terminates a unit test when a child exits non-zero
+ * without offering a means of disabling this behavior locally. All we
+ * have is below macro which isn’t even available on older versions of
+ * the unittest runner.
+ */
+#   define BOOST_TEST_IGNORE_NON_ZERO_CHILD_CODE
+#else
+/* Boost too old; skip test that validate error handling. */
+#   define NO_CHILD_FAIL_TESTS
+#endif
+
+#define BOOST_TEST_DYN_LINK
+#include <boost/test/unit_test.hpp>
+
+#include "stringfunc.hxx"
+#include "pipestream.hxx"
+
+#define TO_CHARP_TOK(x) #x
+#define TO_CHARP(x) TO_CHARP_TOK(x)
+
+#define I2N_EXTRA_ENV "I2N_EXTRA_ENV"
+
+struct Test_Pipestream_Fixture
+{
+    char **saved_environ;
+
+    Test_Pipestream_Fixture (void) : saved_environ (environ) { }
+
+    ~Test_Pipestream_Fixture (void) {
+        environ = this->saved_environ;
+        (void)unsetenv (I2N_EXTRA_ENV);
+    }
+};
+
+BOOST_FIXTURE_TEST_SUITE(pipestream, Test_Pipestream_Fixture)
+
+BOOST_AUTO_TEST_SUITE(read)
+
+# define ENOUGH_ZEROS 42
+const char *const zero_bytes_argv [] =
+    { "/usr/bin/head", "-c", TO_CHARP(ENOUGH_ZEROS), "/dev/zero", NULL };
+
+BOOST_AUTO_TEST_CASE(abspath_zeros_shell_ok)
+{
+    const std::string result =
+        capture_exec (I2n::join_string (zero_bytes_argv, " "));
+
+    BOOST_CHECK_EQUAL(result.size (), ENOUGH_ZEROS);
+}
+
+BOOST_AUTO_TEST_CASE(abspath_zeros_shell_ok_result)
+{
+    ExecResult exres = ExecResult ();
+    const std::string result =
+        capture_exec (I2n::join_string (zero_bytes_argv, " "), exres);
+
+    BOOST_CHECK(exres.normal_exit);
+    BOOST_CHECK_EQUAL(exres.return_code, 0);
+    BOOST_CHECK(!exres.terminated_by_signal);
+    BOOST_CHECK_EQUAL(result.size (), ENOUGH_ZEROS);
+}
+
+BOOST_AUTO_TEST_CASE(abspath_zeros_noshell_ok)
+{
+    const std::string result = capture_exec (zero_bytes_argv);
+
+    BOOST_CHECK_EQUAL(result.size (), ENOUGH_ZEROS);
+}
+
+BOOST_AUTO_TEST_CASE(abspath_zeros_noshell_ok_strvec)
+{
+    std::vector<std::string> argvec;
+    const char *const *argp = zero_bytes_argv;
+    const char *       cur  = NULL;
+
+    while ((cur = *argp++) != NULL) {
+        argvec.push_back (std::string (cur));
+    }
+
+    const std::string result = capture_exec (argvec);
+
+    BOOST_CHECK_EQUAL(result.size (), ENOUGH_ZEROS);
+}
+
+BOOST_AUTO_TEST_CASE(abspath_zeros_noshell_ok_result)
+{
+    ExecResult exres = ExecResult ();
+    const std::string result = capture_exec (zero_bytes_argv, exres);
+
+    BOOST_CHECK(exres.normal_exit);
+    BOOST_CHECK_EQUAL(exres.return_code, 0);
+    BOOST_CHECK(!exres.terminated_by_signal);
+    BOOST_CHECK_EQUAL(result.size (), ENOUGH_ZEROS);
+}
+
+const char *const bad_command [] = { "/does_not_exist", NULL };
+
+# ifndef NO_CHILD_FAIL_TESTS
+BOOST_AUTO_TEST_CASE(abspath_bad_shell_fail)
+{
+    assert (access(bad_command [0], X_OK) != 0);
+
+    ExecResult exres = ExecResult ();
+    /*
+     * Note that the next line will make the unit test spew a message
+     * to stderr which cannot be prevented due to the limitations of
+     * popen(3).
+     */
+    const std::string result =
+        capture_exec (I2n::join_string (bad_command, " "));
+
+    BOOST_CHECK(!exres.normal_exit);
+    BOOST_CHECK_EQUAL(result.size (), 0);
+}
+# endif /* [!NO_CHILD_FAIL_TESTS] */
+
+# ifndef NO_CHILD_FAIL_TESTS
+BOOST_AUTO_TEST_CASE(abspath_bad_noshell_fail)
+{
+    assert (access(bad_command [0], X_OK) != 0);
+
+    ExecResult exres = ExecResult ();
+    const std::string result = capture_exec (bad_command, exres);
+
+    BOOST_CHECK(!exres.normal_exit); /* failed to exec() */
+    BOOST_CHECK(!exres.terminated_by_signal);
+    BOOST_CHECK_EQUAL(result.size (), 0);
+    BOOST_CHECK_EQUAL(exres.error_message,
+                      "child failed to exec(): "
+                      "error 2 (No such file or directory)");
+}
+# endif /* [!NO_CHILD_FAIL_TESTS] */
+
+# ifndef NO_CHILD_FAIL_TESTS
+BOOST_AUTO_TEST_CASE(abspath_bad_noshell_stderr)
+{
+    assert (access(bad_command [0], X_OK) != 0);
+
+    ExecResult exres = ExecResult ();
+    const std::string result = capture_exec (bad_command, exres,
+                                             capture_flag::collect_err);
+
+    BOOST_CHECK(!exres.normal_exit); /* failed to exec() */
+    BOOST_CHECK(!exres.terminated_by_signal);
+    BOOST_CHECK_EQUAL(result.size (), 0);
+    BOOST_CHECK_EQUAL(exres.error_message,
+                      "child failed to exec(): "
+                      "error 2 (No such file or directory)");
+}
+# endif /* [!NO_CHILD_FAIL_TESTS] */
+
+const char *const false_argv_abs [] = { "/bin/false", NULL };
+const char *const true_argv_abs  [] = { "/bin/true" , NULL };
+const char *const false_argv_rel [] = { "false"     , NULL };
+const char *const true_argv_rel  [] = { "true"      , NULL };
+
+# ifndef NO_CHILD_FAIL_TESTS
+BOOST_AUTO_TEST_CASE(abspath_false_noshell_fail_exit)
+{
+    ExecResult exres = ExecResult ();
+    const std::string result =
+        capture_exec (false_argv_abs, exres, capture_flag::collect_out);
+
+    BOOST_CHECK(exres.normal_exit);
+    BOOST_CHECK_EQUAL(exres.return_code, EXIT_FAILURE);
+    BOOST_CHECK_EQUAL(result.size (), 0);
+}
+# endif /* [!NO_CHILD_FAIL_TESTS] */
+
+# ifndef NO_CHILD_FAIL_TESTS
+BOOST_AUTO_TEST_CASE(abspath_false_shell_fail_exit)
+{
+    ExecResult exres = ExecResult ();
+    const std::string result =
+        capture_exec (std::string (false_argv_abs [0]), exres);
+
+    BOOST_CHECK(exres.normal_exit);
+    BOOST_CHECK_EQUAL(exres.return_code, EXIT_FAILURE);
+    BOOST_CHECK_EQUAL(result.size (), 0);
+}
+# endif /* [!NO_CHILD_FAIL_TESTS] */
+
+BOOST_AUTO_TEST_CASE(relpath_true_noshell_ok)
+{
+    ExecResult exres = ExecResult ();
+    const std::string result =
+        capture_exec (true_argv_rel, exres,
+                      capture_flag::collect_out | capture_flag::search_path);
+
+    BOOST_CHECK(exres.normal_exit);
+    BOOST_CHECK_EQUAL(exres.return_code, EXIT_SUCCESS);
+    BOOST_CHECK_EQUAL(result.size (), 0);
+}
+
+# ifndef NO_CHILD_FAIL_TESTS
+BOOST_AUTO_TEST_CASE(relpath_true_noshell_fail)
+{
+    ExecResult exres = ExecResult ();
+    const std::string result =
+        capture_exec (true_argv_rel, exres, capture_flag::collect_out);
+
+    BOOST_CHECK(!exres.normal_exit); /* failed to exec() */
+    /* no return code check since we couldn't exit */
+    BOOST_CHECK_EQUAL(result.size (), 0);
+    BOOST_CHECK_EQUAL(exres.error_message,
+                      "child failed to exec(): "
+                      "error 2 (No such file or directory)");
+}
+# endif /* [!NO_CHILD_FAIL_TESTS] */
+
+BOOST_AUTO_TEST_CASE(abspath_true_noshell_ok)
+{
+    ExecResult exres = ExecResult ();
+    const std::string result =
+        capture_exec (true_argv_abs, exres,
+                      capture_flag::collect_out | capture_flag::search_path);
+
+    BOOST_CHECK(exres.normal_exit);
+    BOOST_CHECK_EQUAL(exres.return_code, EXIT_SUCCESS);
+    BOOST_CHECK_EQUAL(result.size (), 0);
+}
+
+# ifndef NO_CHILD_FAIL_TESTS
+BOOST_AUTO_TEST_CASE(relpath_false_noshell_fail)
+{
+    ExecResult exres = ExecResult ();
+    const std::string result =
+        capture_exec (false_argv_rel, exres,
+                      capture_flag::collect_out | capture_flag::search_path);
+
+    BOOST_CHECK(exres.normal_exit);
+    /* no return code check since we couldn't exit */
+    BOOST_CHECK_EQUAL(result.size (), 0);
+}
+# endif /* [!NO_CHILD_FAIL_TESTS] */
+
+const char *const echo_abs = "/bin/echo";
+const char *const echo_rel = "echo";
+
+static std::vector<std::string>
+mk_echo_argv (const std::string &text, const bool absolute=true)
+{
+    std::vector<std::string> ret;
+
+    ret.push_back (absolute ? echo_abs : echo_rel);
+    ret.push_back ("-n");
+    ret.push_back (text);
+
+    return ret;
+}
+
+BOOST_AUTO_TEST_CASE(abspath_echo_noshell_capture_ok)
+{
+    ExecResult exres = ExecResult ();
+    const std::string text = "The significant owl hoots in the night.";
+    const std::vector<std::string> argv = mk_echo_argv (text);
+    const std::string result = capture_exec (argv, exres,
+                                             capture_flag::collect_out
+                                             | capture_flag::search_path);
+
+    BOOST_CHECK(exres.normal_exit);
+    BOOST_CHECK_EQUAL(exres.return_code, EXIT_SUCCESS);
+    BOOST_CHECK_EQUAL(result, text);
+}
+
+BOOST_AUTO_TEST_CASE(relpath_echo_noshell_capture_ok)
+{
+    ExecResult exres = ExecResult ();
+    const std::string text = "Yet many grey lords go sadly to the masterless men.";
+    const std::vector<std::string> argv = mk_echo_argv (text, false);
+    const std::string result = capture_exec (argv, exres,
+                                             capture_flag::collect_out
+                                             | capture_flag::search_path);
+
+    BOOST_CHECK(exres.normal_exit);
+    BOOST_CHECK_EQUAL(exres.return_code, EXIT_SUCCESS);
+    BOOST_CHECK_EQUAL(result, text);
+}
+
+static std::vector<std::string>
+mk_errcho_argv (const std::string &text)
+{
+    /*
+     * Hack cause there’s no way to make echo print to stderr without
+     * redirection.
+     */
+    std::vector<std::string> ret;
+
+    ret.push_back ("/bin/sh");
+    ret.push_back ("-c");
+    ret.push_back (std::string ("1>&- 1>&2 echo -n '") + text + "'"); /* brr */
+
+    return ret;
+}
+
+BOOST_AUTO_TEST_CASE(sh_errcho_capture_ok)
+{
+    ExecResult exres = ExecResult ();
+    const std::string text = "Hooray, hooray for the spinster’s sister’s daughter.";
+    const std::vector<std::string> argv = mk_errcho_argv (text);
+    const std::string result = capture_exec (argv, exres,
+                                               capture_flag::collect_err
+                                             | capture_flag::search_path);
+
+    BOOST_CHECK(exres.normal_exit);
+    BOOST_CHECK_EQUAL(exres.return_code, EXIT_SUCCESS);
+    BOOST_CHECK_EQUAL(result, text);
+}
+
+BOOST_AUTO_TEST_CASE(sh_errcho_stdout_empty_ok)
+{
+    ExecResult exres = ExecResult ();
+    const std::string text = "To the axeman, all supplicants are the same height.";
+    const std::vector<std::string> argv = mk_errcho_argv (text);
+    const std::string result = capture_exec (argv, exres,
+                                               capture_flag::collect_out
+                                             | capture_flag::search_path);
+
+    BOOST_CHECK(exres.normal_exit);
+    BOOST_CHECK_EQUAL(exres.return_code, EXIT_SUCCESS);
+    BOOST_CHECK_EQUAL(result.size (), 0);
+}
+
+BOOST_AUTO_TEST_SUITE_END() /* [pipestream->read] */
+
+BOOST_AUTO_TEST_SUITE(env)
+
+static const char *const env_argv_abs [] = { "/usr/bin/env", NULL };
+
+#define I2N_EXTRA_ENVIRON I2N_EXTRA_ENV "=Yet verily, the rose is within the thorn."
+static char *i2n_extra_environ [] =
+    { const_cast<char*> (I2N_EXTRA_ENVIRON) , NULL };
+
+BOOST_AUTO_TEST_CASE(env_passthrough)
+{
+    ExecResult exres = ExecResult ();
+    environ = i2n_extra_environ;
+
+    const std::string result =
+        capture_exec (env_argv_abs, exres,
+                        capture_flag::collect_out
+                      | capture_flag::collect_err
+                      | capture_flag::env_passthru);
+
+    BOOST_CHECK(exres.normal_exit);
+    BOOST_CHECK_EQUAL(exres.return_code, EXIT_SUCCESS);
+    BOOST_CHECK_EQUAL(result, (std::string)(I2N_EXTRA_ENVIRON "\n"));
+}
+
+BOOST_AUTO_TEST_CASE(env_nil)
+{
+    ExecResult exres = ExecResult ();
+    environ = i2n_extra_environ;
+
+    const std::string result =
+        capture_exec (env_argv_abs, exres,
+                        capture_flag::collect_out
+                      | capture_flag::collect_err);
+
+    BOOST_CHECK(exres.normal_exit);
+    BOOST_CHECK_EQUAL(exres.return_code, EXIT_SUCCESS);
+    BOOST_CHECK_EQUAL(result, std::string());
+}
+
+BOOST_AUTO_TEST_SUITE_END() /* [pipestream->env] */
+
+BOOST_AUTO_TEST_SUITE(privs)
+
+#define I2N_EXPECT_OUTPUT "the caged whale knows nothing of the mighty deeps"
+const char *const echo_argv [] = { "echo", I2N_EXPECT_OUTPUT, NULL };
+
+/*
+ * this is not as such a functionality test, in the sense that
+ * we can’t easily (let alone portably) test the behavior of suid
+ * binaries. thus we only check that the option is indeed accepted
+ * in a trivial case.
+ */
+BOOST_AUTO_TEST_CASE(no_new_privs)
+{
+    ExecResult exres = ExecResult ();
+
+    const std::string result = capture_exec (echo_argv, exres,
+                                               capture_flag::collect_out
+                                             | capture_flag::search_path
+                                             | capture_flag::no_new_privs);
+
+    BOOST_CHECK(exres.normal_exit);
+    BOOST_CHECK_EQUAL(exres.return_code, EXIT_SUCCESS);
+    BOOST_CHECK_EQUAL(result, (std::string)I2N_EXPECT_OUTPUT + "\n");
+}
+
+BOOST_AUTO_TEST_SUITE_END() /* [pipestream->privs] */
+
+BOOST_AUTO_TEST_SUITE_END() /* [pipestream] */
+