/* Copyright 2018 The ChromiumOS Authors
 * Use of this source code is governed by a BSD-style license that can be
 * found in the LICENSE file.
 *
 * Test the minijail0 CLI using gtest.
 *
 * Note: We don't verify that the minijail struct was set correctly from these
 * flags as only libminijail.c knows that definition.  If we wanted to improve
 * this test, we'd have to pull that struct into a common (internal) header.
 */

#include <cstdio>
#include <fstream>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#include <gtest/gtest.h>

#include "config_parser.h"
#include "libminijail-private.h"
#include "libminijail.h"
#include "minijail0_cli.h"
#include "test_util.h"
#include "util.h"

extern "C" char** environ;

namespace {

constexpr char kValidUser[] = "nobody";
constexpr char kValidUid[] = "100";
constexpr char kValidGroup[] = "users";
constexpr char kValidGid[] = "100";

bool GetEnvValue(const std::vector<std::string>& env,
                 const std::string& key,
                 std::string* value_out = nullptr) {
  const std::string prefix = key + "=";
  for (const auto& entry : env) {
    if (entry.rfind(prefix, 0) == 0) {
      if (value_out)
        *value_out = entry.substr(prefix.size());
      return true;
    }
  }
  return false;
}

class CliTest : public ::testing::Test {
 protected:
  virtual void SetUp() {
    // Most tests do not care about this logic.  For the few that do, make
    // them opt into it so they can validate specifically.
    elftype_ = ELFDYNAMIC;
  }
  virtual void TearDown() {}

  // We use a vector of strings rather than const char * pointers because we
  // need the backing memory to be writable.  The CLI might mutate the strings
  // as it parses things (which is normally permissible with argv).
  int parse_args_(const std::vector<std::string>& argv,
                  bool* exit_immediately,
                  ElfType* elftype,
                  std::vector<std::string>* child_env = nullptr) {
    // Make sure we reset the getopts state when scanning a new argv.  Setting
    // this to 0 is a GNU extension, but AOSP/BSD also checks this (as an alias
    // to their "optreset").
    optind = 0;

    // We create & destroy this for every parse_args call because some API
    // calls can dupe memory which confuses LSAN.  https://crbug.com/844615
    struct minijail* j = minijail_new();

    std::vector<const char*> pargv;
    pargv.push_back("minijail0");
    for (const std::string& arg : argv)
      pargv.push_back(arg.c_str());

    // We grab stdout from parse_args itself as it might dump things we don't
    // usually care about like help output.
    testing::internal::CaptureStdout();

    const char* preload_path = PRELOADPATH;
    const char* default_preload_path = preload_path;
    char** envp = NULL;
    int ret =
        parse_args(j, pargv.size(), const_cast<char* const*>(pargv.data()),
                   NULL, exit_immediately, elftype, &preload_path, &envp);
    testing::internal::GetCapturedStdout();

    if (child_env) {
      child_env->clear();
      if (envp) {
        for (char** entry = envp; *entry; ++entry)
          child_env->emplace_back(*entry);
      }
    }

    if (envp && envp != environ)
      minijail_free_env(envp);

    if (preload_path && preload_path != default_preload_path)
      free(const_cast<char*>(preload_path));

    minijail_destroy(j);

    return ret;
  }

  int parse_args_(const std::vector<std::string>& argv,
                  std::vector<std::string>* child_env) {
    return parse_args_(argv, &exit_immediately_, &elftype_, child_env);
  }

  int parse_args_(const std::vector<std::string>& argv) {
    return parse_args_(argv, &exit_immediately_, &elftype_, nullptr);
  }

  ElfType elftype_;
  bool exit_immediately_;
};

}  // namespace

// Should exit non-zero when there's no arguments.
TEST_F(CliTest, no_args) {
  std::vector<std::string> argv = {};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}

// Should exit zero when we asked for help.
TEST_F(CliTest, help) {
  std::vector<std::string> argv = {"-h"};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(0), "");

  argv = {"--help"};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(0), "");

  argv = {"-H"};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(0), "");
}

// Just a simple program to run.
TEST_F(CliTest, valid_program) {
  std::vector<std::string> argv = {"/bin/sh"};
  ASSERT_TRUE(parse_args_(argv));
}

// Valid calls to the change user option.
TEST_F(CliTest, valid_set_user) {
  std::vector<std::string> argv = {"-u", "", "/bin/sh"};

  argv[1] = kValidUser;
  ASSERT_TRUE(parse_args_(argv));

  argv[1] = kValidUid;
  ASSERT_TRUE(parse_args_(argv));
}

// Invalid calls to the change user option.
TEST_F(CliTest, invalid_set_user) {
  std::vector<std::string> argv = {"-u", "", "/bin/sh"};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  argv[1] = "j;lX:J*Pj;oijfs;jdlkjC;j";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  argv[1] = "1000x";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  // Supplying -u more than once is bad.
  argv = {"-u", kValidUser, "-u", kValidUid, "/bin/sh"};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1),
              "-u provided multiple times");
}

// Valid calls to the change group option.
TEST_F(CliTest, valid_set_group) {
  std::vector<std::string> argv = {"-g", "", "/bin/sh"};

  argv[1] = kValidGroup;
  ASSERT_TRUE(parse_args_(argv));

  argv[1] = kValidGid;
  ASSERT_TRUE(parse_args_(argv));
}

// Invalid calls to the change group option.
TEST_F(CliTest, invalid_set_group) {
  std::vector<std::string> argv = {"-g", "", "/bin/sh"};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  argv[1] = "j;lX:J*Pj;oijfs;jdlkjC;j";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  argv[1] = "1000x";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  // Supplying -g more than once is bad.
  argv = {"-g", kValidGroup, "-g", kValidGid, "/bin/sh"};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1),
              "-g provided multiple times");
}

// Valid calls to the add-suppl-group option.
TEST_F(CliTest, valid_add_supp_group) {
  std::vector<std::string> argv = {"--add-suppl-group", "", "/bin/sh"};

  argv[1] = kValidGroup;
  ASSERT_TRUE(parse_args_(argv));

  argv[1] = kValidGid;
  ASSERT_TRUE(parse_args_(argv));

  std::vector<std::string> argv2 = {"--add-suppl-group", "",
                                    "--add-suppl-group", "", "/bin/sh"};
  argv[1] = kValidGroup;
  argv[2] = kValidGid;
  ASSERT_TRUE(parse_args_(argv));
}

// Invalid calls to the add-suppl-group option.
TEST_F(CliTest, invalid_add_supp_group) {
  std::vector<std::string> argv = {"--add-suppl-group", "", "/bin/sh"};

  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  argv[1] = "j;lX:J*Pj;oijfs;jdlkjC;j";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  argv[1] = "1000x";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}

// Valid calls to the skip securebits option.
TEST_F(CliTest, valid_skip_securebits) {
  // An empty string is the same as 0.
  std::vector<std::string> argv = {"-B", "", "/bin/sh"};
  ASSERT_TRUE(parse_args_(argv));

  argv[1] = "0xAB";
  ASSERT_TRUE(parse_args_(argv));

  argv[1] = "1234";
  ASSERT_TRUE(parse_args_(argv));
}

// Invalid calls to the skip securebits option.
TEST_F(CliTest, invalid_skip_securebits) {
  std::vector<std::string> argv = {"-B", "", "/bin/sh"};

  argv[1] = "xja";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}

// Valid calls to the caps option.
TEST_F(CliTest, valid_caps) {
  // An empty string is the same as 0.
  std::vector<std::string> argv = {"-c", "", "/bin/sh"};
  ASSERT_TRUE(parse_args_(argv));

  argv[1] = "0xAB";
  ASSERT_TRUE(parse_args_(argv));

  argv[1] = "1234";
  ASSERT_TRUE(parse_args_(argv));
}

// Invalid calls to the caps option.
TEST_F(CliTest, invalid_caps) {
  std::vector<std::string> argv = {"-c", "", "/bin/sh"};

  argv[1] = "xja";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}

// Valid calls to the logging option.
TEST_F(CliTest, valid_logging) {
  std::vector<std::string> argv = {"--logging", "", "/bin/sh"};

  // This should list all valid logging targets.
  const std::vector<std::string> profiles = {
      "stderr",
      "syslog",
  };

  for (const auto& profile : profiles) {
    argv[1] = profile;
    ASSERT_TRUE(parse_args_(argv));
  }
}

// Invalid calls to the logging option.
TEST_F(CliTest, invalid_logging) {
  std::vector<std::string> argv = {"--logging", "", "/bin/sh"};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  argv[1] = "stdout";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}

// Valid calls to the rlimit option.
TEST_F(CliTest, valid_rlimit) {
  std::vector<std::string> argv = {"-R", "", "/bin/sh"};

  argv[1] = "0,1,2";
  ASSERT_TRUE(parse_args_(argv));

  argv[1] = "0,0x100,4";
  ASSERT_TRUE(parse_args_(argv));

  argv[1] = "1,1,unlimited";
  ASSERT_TRUE(parse_args_(argv));

  argv[1] = "2,unlimited,2";
  ASSERT_TRUE(parse_args_(argv));

  argv[1] = "RLIMIT_AS,unlimited,unlimited";
  ASSERT_TRUE(parse_args_(argv));
}

// Invalid calls to the rlimit option.
TEST_F(CliTest, invalid_rlimit) {
  std::vector<std::string> argv = {"-R", "", "/bin/sh"};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  // Missing cur & max.
  argv[1] = "0";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  // Missing max.
  argv[1] = "0,0";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  // Too many options.
  argv[1] = "0,0,0,0";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  // Non-numeric limits
  argv[1] = "0,0,invalid-limit";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  // Invalid number.
  argv[1] = "0,0,0j";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  // Invalid hex number.
  argv[1] = "0,0x1jf,0";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  // Invalid rlimit constant.
  argv[1] = "RLIMIT_GOGOOGOG,0,0";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}

// Valid calls to the profile option.
TEST_F(CliTest, valid_profile) {
  std::vector<std::string> argv = {"--profile", "", "/bin/sh"};

  // This should list all valid profiles.
  const std::vector<std::string> profiles = {
      "minimalistic-mountns",
      "minimalistic-mountns-nodev",
  };

  for (const auto& profile : profiles) {
    argv[1] = profile;
    ASSERT_TRUE(parse_args_(argv));
  }
}

// Invalid calls to the profile option.
TEST_F(CliTest, invalid_profile) {
  std::vector<std::string> argv = {"--profile", "", "/bin/sh"};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  argv[1] = "random-unknown-profile";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}

// Valid usage of the no-fs-restrictions option.
TEST_F(CliTest, valid_no_fs_restrictions) {
  std::vector<std::string> argv = {"--profile", "minimalistic-mountns",
                                   "--no-fs-restrictions", "/bin/sh"};

  ASSERT_TRUE(parse_args_(argv));
}

// Invalid usage of the no-fs-restrictions option.
TEST_F(CliTest, invalid_no_fs_restrictions) {
  // Using an fs-path-* flag at the same time shouldn't be allowed.
  std::vector<std::string> argv = {"--fs-path-rx", "/", "--no-fs-restrictions",
                                   "/bin/sh"};

  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}

// Valid calls to the chroot option.
TEST_F(CliTest, valid_chroot) {
  std::vector<std::string> argv = {"-C", "/", "/bin/sh"};
  ASSERT_TRUE(parse_args_(argv));
}

// Valid calls to the pivot root option.
TEST_F(CliTest, valid_pivot_root) {
  std::vector<std::string> argv = {"-P", "/", "/bin/sh"};
  ASSERT_TRUE(parse_args_(argv));
}

// We cannot handle multiple options with chroot/profile/pivot root.
TEST_F(CliTest, conflicting_roots) {
  std::vector<std::string> argv;

  // Chroot & pivot root.
  argv = {"-C", "/", "-P", "/", "/bin/sh"};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  // Chroot & minimalistic-mountns profile.
  argv = {"-C", "/", "--profile", "minimalistic-mountns", "/bin/sh"};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  // Pivot root & minimalistic-mountns profile.
  argv = {"-P", "/", "--profile", "minimalistic-mountns", "/bin/sh"};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}

// Valid calls to the uidmap option.
TEST_F(CliTest, valid_uidmap) {
  std::vector<std::string> argv = {"-m", "/bin/sh"};
  // Use a default map (no option from user).
  ASSERT_TRUE(parse_args_(argv));

  // Use a single map.
  argv = {"-m0 0 1", "/bin/sh"};
  ASSERT_TRUE(parse_args_(argv));

  // Multiple maps.
  argv = {"-m0 0 1,100 100 1", "/bin/sh"};
  ASSERT_TRUE(parse_args_(argv));
}

// Valid calls to the gidmap option.
TEST_F(CliTest, valid_gidmap) {
  std::vector<std::string> argv = {"-M", "/bin/sh"};
  // Use a default map (no option from user).
  ASSERT_TRUE(parse_args_(argv));

  // Use a single map.
  argv = {"-M0 0 1", "/bin/sh"};
  ASSERT_TRUE(parse_args_(argv));

  // Multiple maps.
  argv = {"-M0 0 1,100 100 1", "/bin/sh"};
  ASSERT_TRUE(parse_args_(argv));
}

// Invalid calls to the uidmap/gidmap options.
// Note: Can't really test these as all validation is delayed/left to the
// runtime kernel.  Minijail will simply write verbatim what the user gave
// it to the corresponding /proc/.../[ug]id_map.

// Valid calls to the binding option.
TEST_F(CliTest, valid_binding) {
  std::vector<std::string> argv = {"-v", "-b", "", "/bin/sh"};

  // Dest & writable are optional.
  argv[1] = "/";
  ASSERT_TRUE(parse_args_(argv));

  // Writable is optional.
  argv[1] = "/,/";
  ASSERT_TRUE(parse_args_(argv));

  // Writable is an integer.
  argv[1] = "/,/,0";
  ASSERT_TRUE(parse_args_(argv));
  argv[1] = "/,/,1";
  ASSERT_TRUE(parse_args_(argv));

  // Dest is optional.
  argv[1] = "/,,0";
  ASSERT_TRUE(parse_args_(argv));
}

// Invalid calls to the binding option.
TEST_F(CliTest, invalid_binding) {
  std::vector<std::string> argv = {"-v", "-b", "", "/bin/sh"};

  // Missing source.
  argv[2] = "";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  // Too many args.
  argv[2] = "/,/,0,what";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  // Missing mount namespace/etc...
  argv = {"-b", "/", "/bin/sh"};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  // Bad value for <writable>.
  argv = {"-b", "/,,writable", "/bin/sh"};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}

// Valid calls to the mount option.
TEST_F(CliTest, valid_mount) {
  std::vector<std::string> argv = {"-v", "-k", "", "/bin/sh"};

  // Flags & data are optional.
  argv[2] = "none,/,none";
  ASSERT_TRUE(parse_args_(argv));

  // Data is optional.
  argv[2] = "none,/,none,0xe";
  ASSERT_TRUE(parse_args_(argv));

  // Flags are optional.
  argv[2] = "none,/,none,,mode=755";
  ASSERT_TRUE(parse_args_(argv));

  // Multiple data options to the kernel.
  argv[2] = "none,/,none,0xe,mode=755,uid=0,gid=10";
  ASSERT_TRUE(parse_args_(argv));

  // Single MS constant.
  argv[2] = "none,/,none,MS_NODEV,mode=755";
  ASSERT_TRUE(parse_args_(argv));

  // Multiple MS constants.
  argv[2] = "none,/,none,MS_NODEV|MS_NOEXEC,mode=755";
  ASSERT_TRUE(parse_args_(argv));

  // Mixed constant & number.
  argv[2] = "none,/,none,MS_NODEV|0xf,mode=755";
  ASSERT_TRUE(parse_args_(argv));
}

// Invalid calls to the mount option.
TEST_F(CliTest, invalid_mount) {
  std::vector<std::string> argv = {"-v", "-k", "", "/bin/sh"};

  // Missing source.
  argv[2] = "";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  // Missing dest.
  argv[2] = "none";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  // Missing type.
  argv[2] = "none,/";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  // Unknown MS constant.
  argv[2] = "none,/,none,MS_WHOOPS";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}

// Valid calls to the remount mode option.
TEST_F(CliTest, valid_remount_mode) {
  std::vector<std::string> argv = {"-v", "", "/bin/sh"};

  // Mode is optional.
  argv[1] = "-K";
  ASSERT_TRUE(parse_args_(argv));

  // This should list all valid modes.
  const std::vector<std::string> modes = {
      "shared",
      "private",
      "slave",
      "unbindable",
  };

  for (const auto& mode : modes) {
    argv[1] = "-K" + mode;
    ASSERT_TRUE(parse_args_(argv));
  }
}

// Invalid calls to the remount mode option.
TEST_F(CliTest, invalid_remount_mode) {
  std::vector<std::string> argv = {"-v", "", "/bin/sh"};

  // Unknown mode.
  argv[1] = "-Kfoo";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}

TEST_F(CliTest, invalid_L_combo) {
  std::vector<std::string> argv = {"", "", "", "/bin/sh"};

  // Cannot call minijail0 with -L and a pre-compiled seccomp policy.
  argv[0] = "-L";
  argv[1] = "--seccomp-bpf-binary";
  argv[2] = "source";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");

  argv[0] = "--seccomp-bpf-binary";
  argv[1] = "source";
  argv[2] = "-L";
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}

// Valid calls to the clear env option.
TEST_F(CliTest, valid_clear_env) {
  std::vector<std::string> argv = {"--env-reset", "/bin/sh"};

  ASSERT_TRUE(parse_args_(argv));
}

// Valid calls to the set env option.
TEST_F(CliTest, valid_set_env) {
  std::vector<std::string> argv1 = {"--env-add", "NAME=value", "/bin/sh"};
  ASSERT_TRUE(parse_args_(argv1));

  // multiple occurences are allowed.
  std::vector<std::string> argv2 = {"--env-add", "A=b", "--env-add", "b=C=D",
                                    "/bin/sh"};
  ASSERT_TRUE(parse_args_(argv2));

  // --env-reset before any --env-add to not pass our own env.
  std::vector<std::string> argv3 = {"--env-reset", "--env-add", "A=b",
                                    "/bin/sh"};
  ASSERT_TRUE(parse_args_(argv3));

  // --env-add before an --env-reset doesn't have any effect, but is allowed.
  std::vector<std::string> argv4 = {"--env-add", "A=b", "--env-reset",
                                    "/bin/sh"};
  ASSERT_TRUE(parse_args_(argv4));
}

// Invalid calls to the set env options.
TEST_F(CliTest, invalid_set_env) {
  // invalid env=value arguments.
  std::vector<std::string> argv2 = {"--env-add", "", "/bin/sh"};

  argv2[1] = "INVALID";
  ASSERT_EXIT(parse_args_(argv2), testing::ExitedWithCode(1), "");

  argv2[1] = "=";
  ASSERT_EXIT(parse_args_(argv2), testing::ExitedWithCode(1), "");

  argv2[1] = "=foo";
  ASSERT_EXIT(parse_args_(argv2), testing::ExitedWithCode(1), "");
}

TEST_F(CliTest, logging_stderr_sets_env_for_dynamic_targets) {
  std::vector<std::string> argv = {"--logging=stderr", "/bin/sh"};
  ElfType elftype = ELFDYNAMIC;
  bool exit_immediately = false;
  std::vector<std::string> child_env;

  ASSERT_TRUE(parse_args_(argv, &exit_immediately, &elftype, &child_env));

  std::string value;
  ASSERT_TRUE(GetEnvValue(child_env, kLoggingEnvVar, &value));
  EXPECT_EQ(value, kLoggingEnvValueStderr);
}

TEST_F(CliTest, logging_stderr_skips_env_for_static_targets) {
  std::vector<std::string> argv = {"--logging=stderr", "/bin/sh"};
  ElfType elftype = ELFSTATIC;
  bool exit_immediately = false;
  std::vector<std::string> child_env;

  ASSERT_TRUE(parse_args_(argv, &exit_immediately, &elftype, &child_env));

  EXPECT_FALSE(GetEnvValue(child_env, kLoggingEnvVar));
}

TEST_F(CliTest, logging_auto_defaults_to_syslog_without_tty) {
  if (isatty(STDIN_FILENO)) {
    GTEST_SKIP() << "stdin is a TTY; cannot assert auto behavior";
  }

  std::vector<std::string> argv = {"--logging=auto", "/bin/sh"};
  ElfType elftype = ELFDYNAMIC;
  bool exit_immediately = false;
  std::vector<std::string> child_env;

  ASSERT_TRUE(parse_args_(argv, &exit_immediately, &elftype, &child_env));

  EXPECT_FALSE(GetEnvValue(child_env, kLoggingEnvVar));
}

TEST_F(CliTest, logging_stderr_respects_env_reset) {
  std::vector<std::string> argv = {"--env-reset", "--logging=stderr",
                                   "/bin/sh"};
  ElfType elftype = ELFDYNAMIC;
  bool exit_immediately = false;
  std::vector<std::string> child_env;

  ASSERT_TRUE(parse_args_(argv, &exit_immediately, &elftype, &child_env));

  std::string value;
  ASSERT_TRUE(GetEnvValue(child_env, kLoggingEnvVar, &value));
  EXPECT_EQ(value, kLoggingEnvValueStderr);
}

// Valid calls to the gen-config option.
TEST_F(CliTest, valid_gen_config) {
  std::string config_path = std::tmpnam(NULL);
  std::vector<std::string> argv = {"--gen-config=" + config_path, "--ambient",
                                   "--fs-path-rx=/", "-n"};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(0), "");
}

TEST_F(CliTest, valid_gen_config_with_users_mounts_not_found) {
  std::string config_path = std::tmpnam(NULL);
  std::vector<std::string> argv = {"--gen-config=" + config_path,
                                   "--ambient",
                                   "-u fake-user",
                                   "-g fake-group",
                                   "-b /fake-mount",
                                   "-k /fake-mount",
                                   "-e /fake-path",
                                   "-f /fake-path",
                                   "-V /fake-path",
                                   "-n"};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(0), "");
}

// Invalid calls to the gen-config option.
TEST_F(CliTest, invalid_gen_config) {
  std::vector<std::string> argv = {"--gen-config=/", "--ambient",
                                   "--fs-path-rx=/", "-n"};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}

TEST_F(CliTest, invalid_gen_config_not_writable) {
  std::vector<std::string> argv = {"--gen-config=/sys/foo", "--ambient",
                                   "--fs-path-rx=/", "-n"};
  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}

// Android unit tests do not support data file yet.
#if !defined(__ANDROID__)

TEST_F(CliTest, conf_parsing_invalid_key) {
  std::vector<std::string> argv = {"--config", source_path("test/invalid.conf"),
                                   "/bin/sh"};

  ASSERT_EXIT(parse_args_(argv), testing::ExitedWithCode(1), "");
}

TEST_F(CliTest, conf_parsing) {
  std::vector<std::string> argv = {"--config", source_path("test/valid.conf"),
                                   "/bin/sh"};

  ASSERT_TRUE(parse_args_(argv));
}

TEST_F(CliTest, conf_parsing_seccomp) {
  std::string seccomp_path = std::tmpnam(NULL);
  std::ofstream seccomp_stream(seccomp_path);
  // Intentionally empty policy.
  std::string config_path = std::tmpnam(NULL);
  std::ofstream config_stream(config_path);
  config_stream << "% minijail-config-file v0\n"
                   "S = " +
                       seccomp_path;
  config_stream.flush();

  std::vector<std::string> argv = {"--config", config_path, "/bin/sh"};

  ASSERT_TRUE(parse_args_(argv));
}

TEST_F(CliTest, conf_parsing_with_dac_override) {
  std::vector<std::string> argv = {"-c 2", "--config",
                                   source_path("test/valid.conf"), "/bin/sh"};

  ASSERT_TRUE(parse_args_(argv));
}

TEST_F(CliTest, conf_fs_path) {
  std::vector<std::string> argv = {
      "-c 2", "--config", source_path("test/landlock.conf"), "/bin/sh"};

  ASSERT_TRUE(parse_args_(argv));
}

#endif  // !__ANDROID__
