/* Copyright (c) 2018-2021, The Tor Project, Inc. */
/* See LICENSE for licensing information */

/**
 * \file test_process.c
 * \brief Test cases for the Process API.
 */

#include "orconfig.h"
#include "core/or/or.h"
#include "test/test.h"
#include "lib/process/env.h"

#define PROCESS_PRIVATE
#include "lib/process/process.h"
#define PROCESS_UNIX_PRIVATE
#include "lib/process/process_unix.h"
#define PROCESS_WIN32_PRIVATE
#include "lib/process/process_win32.h"

static const char *stdout_read_buffer;
static const char *stderr_read_buffer;

struct process_data_t {
  smartlist_t *stdout_data;
  smartlist_t *stderr_data;
  smartlist_t *stdin_data;
  process_exit_code_t exit_code;
};

typedef struct process_data_t process_data_t;

static process_data_t *
process_data_new(void)
{
  process_data_t *process_data = tor_malloc_zero(sizeof(process_data_t));
  process_data->stdout_data = smartlist_new();
  process_data->stderr_data = smartlist_new();
  process_data->stdin_data = smartlist_new();
  return process_data;
}

static void
process_data_free(process_data_t *process_data)
{
  if (process_data == NULL)
    return;

  SMARTLIST_FOREACH(process_data->stdout_data, char *, x, tor_free(x));
  SMARTLIST_FOREACH(process_data->stderr_data, char *, x, tor_free(x));
  SMARTLIST_FOREACH(process_data->stdin_data, char *, x, tor_free(x));

  smartlist_free(process_data->stdout_data);
  smartlist_free(process_data->stderr_data);
  smartlist_free(process_data->stdin_data);
  tor_free(process_data);
}

static int
process_mocked_read_stdout(process_t *process, buf_t *buffer)
{
  (void)process;

  if (stdout_read_buffer != NULL) {
    buf_add_string(buffer, stdout_read_buffer);
    stdout_read_buffer = NULL;
  }

  return (int)buf_datalen(buffer);
}

static int
process_mocked_read_stderr(process_t *process, buf_t *buffer)
{
  (void)process;

  if (stderr_read_buffer != NULL) {
    buf_add_string(buffer, stderr_read_buffer);
    stderr_read_buffer = NULL;
  }

  return (int)buf_datalen(buffer);
}

static void
process_mocked_write_stdin(process_t *process, buf_t *buffer)
{
  const size_t size = buf_datalen(buffer);

  if (size == 0)
    return;

  char *data = tor_malloc_zero(size + 1);
  process_data_t *process_data = process_get_data(process);

  buf_get_bytes(buffer, data, size);
  smartlist_add(process_data->stdin_data, data);
}

static void
process_stdout_callback(process_t *process, const char *data, size_t size)
{
  tt_ptr_op(process, OP_NE, NULL);
  tt_ptr_op(data, OP_NE, NULL);
  tt_int_op(strlen(data), OP_EQ, size);

  process_data_t *process_data = process_get_data(process);
  smartlist_add(process_data->stdout_data, tor_strdup(data));

 done:
  return;
}

static void
process_stderr_callback(process_t *process, const char *data, size_t size)
{
  tt_ptr_op(process, OP_NE, NULL);
  tt_ptr_op(data, OP_NE, NULL);
  tt_int_op(strlen(data), OP_EQ, size);

  process_data_t *process_data = process_get_data(process);
  smartlist_add(process_data->stderr_data, tor_strdup(data));

 done:
  return;
}

static bool
process_exit_callback(process_t *process, process_exit_code_t exit_code)
{
  tt_ptr_op(process, OP_NE, NULL);

  process_data_t *process_data = process_get_data(process);
  process_data->exit_code = exit_code;

 done:
  /* Do not free up our process_t. */
  return false;
}

static void
test_default_values(void *arg)
{
  (void)arg;
  process_t *process = process_new("/path/to/nothing");

  /* We are not running by default. */
  tt_int_op(PROCESS_STATUS_NOT_RUNNING, OP_EQ, process_get_status(process));

  /* We use the line protocol by default. */
  tt_int_op(PROCESS_PROTOCOL_LINE, OP_EQ, process_get_protocol(process));

  /* We don't set any custom data by default. */
  tt_ptr_op(NULL, OP_EQ, process_get_data(process));

  /* Our command was given to the process_t's constructor in process_new(). */
  tt_str_op("/path/to/nothing", OP_EQ, process_get_command(process));

  /* Make sure we are listed in the list of processes. */
  tt_assert(smartlist_contains(process_get_all_processes(),
                               process));

  /* Default PID is 0. */
  tt_u64_op(0, OP_EQ, process_get_pid(process));

  /* Our arguments should be empty. */
  tt_int_op(0, OP_EQ,
            smartlist_len(process_get_arguments(process)));

 done:
  process_free(process);
}

static void
test_environment(void *arg)
{
  (void)arg;

  process_t *process = process_new("");
  process_environment_t *env = NULL;

  process_set_environment(process, "E", "F");
  process_set_environment(process, "C", "D");
  process_set_environment(process, "A", "B");

  env = process_get_environment(process);
  tt_mem_op(env->windows_environment_block, OP_EQ,
            "A=B\0C=D\0E=F\0", 12);
  tt_str_op(env->unixoid_environment_block[0], OP_EQ,
            "A=B");
  tt_str_op(env->unixoid_environment_block[1], OP_EQ,
            "C=D");
  tt_str_op(env->unixoid_environment_block[2], OP_EQ,
            "E=F");
  tt_ptr_op(env->unixoid_environment_block[3], OP_EQ,
            NULL);
  process_environment_free(env);

  /* Reset our environment. */
  smartlist_t *new_env = smartlist_new();
  smartlist_add(new_env, (char *)"FOO=bar");
  smartlist_add(new_env, (char *)"HELLO=world");

  process_reset_environment(process, new_env);
  smartlist_free(new_env);

  env = process_get_environment(process);
  tt_mem_op(env->windows_environment_block, OP_EQ,
            "FOO=bar\0HELLO=world\0", 20);
  tt_str_op(env->unixoid_environment_block[0], OP_EQ,
            "FOO=bar");
  tt_str_op(env->unixoid_environment_block[1], OP_EQ,
            "HELLO=world");
  tt_ptr_op(env->unixoid_environment_block[2], OP_EQ,
            NULL);

 done:
  process_environment_free(env);
  process_free(process);
}

static void
test_stringified_types(void *arg)
{
  (void)arg;

  /* process_protocol_t values. */
  tt_str_op("Raw", OP_EQ, process_protocol_to_string(PROCESS_PROTOCOL_RAW));
  tt_str_op("Line", OP_EQ, process_protocol_to_string(PROCESS_PROTOCOL_LINE));

  /* process_status_t values. */
  tt_str_op("not running", OP_EQ,
            process_status_to_string(PROCESS_STATUS_NOT_RUNNING));
  tt_str_op("running", OP_EQ,
            process_status_to_string(PROCESS_STATUS_RUNNING));
  tt_str_op("error", OP_EQ,
            process_status_to_string(PROCESS_STATUS_ERROR));

 done:
  return;
}

static void
test_line_protocol_simple(void *arg)
{
  (void)arg;

  process_data_t *process_data = process_data_new();

  process_t *process = process_new("");
  process_set_data(process, process_data);

  process_set_stdout_read_callback(process, process_stdout_callback);
  process_set_stderr_read_callback(process, process_stderr_callback);

  MOCK(process_read_stdout, process_mocked_read_stdout);
  MOCK(process_read_stderr, process_mocked_read_stderr);

  /* Make sure we are running with the line protocol. */
  tt_int_op(PROCESS_PROTOCOL_LINE, OP_EQ, process_get_protocol(process));

  tt_int_op(0, OP_EQ, smartlist_len(process_data->stdout_data));
  tt_int_op(0, OP_EQ, smartlist_len(process_data->stderr_data));

  stdout_read_buffer = "Hello stdout\n";
  process_notify_event_stdout(process);
  tt_ptr_op(NULL, OP_EQ, stdout_read_buffer);

  stderr_read_buffer = "Hello stderr\r\n";
  process_notify_event_stderr(process);
  tt_ptr_op(NULL, OP_EQ, stderr_read_buffer);

  /* Data should be ready. */
  tt_int_op(1, OP_EQ, smartlist_len(process_data->stdout_data));
  tt_int_op(1, OP_EQ, smartlist_len(process_data->stderr_data));

  /* Check if the data is correct. */
  tt_str_op(smartlist_get(process_data->stdout_data, 0), OP_EQ,
            "Hello stdout");
  tt_str_op(smartlist_get(process_data->stderr_data, 0), OP_EQ,
            "Hello stderr");

 done:
  process_data_free(process_data);
  process_free(process);

  UNMOCK(process_read_stdout);
  UNMOCK(process_read_stderr);
}

static void
test_line_protocol_multi(void *arg)
{
  (void)arg;

  process_data_t *process_data = process_data_new();

  process_t *process = process_new("");
  process_set_data(process, process_data);
  process_set_stdout_read_callback(process, process_stdout_callback);
  process_set_stderr_read_callback(process, process_stderr_callback);

  MOCK(process_read_stdout, process_mocked_read_stdout);
  MOCK(process_read_stderr, process_mocked_read_stderr);

  /* Make sure we are running with the line protocol. */
  tt_int_op(PROCESS_PROTOCOL_LINE, OP_EQ, process_get_protocol(process));

  tt_int_op(0, OP_EQ, smartlist_len(process_data->stdout_data));
  tt_int_op(0, OP_EQ, smartlist_len(process_data->stderr_data));

  stdout_read_buffer = "Hello stdout\r\nOnion Onion Onion\nA B C D\r\n\r\n";
  process_notify_event_stdout(process);
  tt_ptr_op(NULL, OP_EQ, stdout_read_buffer);

  stderr_read_buffer = "Hello stderr\nFoo bar baz\nOnion Onion Onion\n";
  process_notify_event_stderr(process);
  tt_ptr_op(NULL, OP_EQ, stderr_read_buffer);

  /* Data should be ready. */
  tt_int_op(4, OP_EQ, smartlist_len(process_data->stdout_data));
  tt_int_op(3, OP_EQ, smartlist_len(process_data->stderr_data));

  /* Check if the data is correct. */
  tt_str_op(smartlist_get(process_data->stdout_data, 0), OP_EQ,
            "Hello stdout");
  tt_str_op(smartlist_get(process_data->stdout_data, 1), OP_EQ,
            "Onion Onion Onion");
  tt_str_op(smartlist_get(process_data->stdout_data, 2), OP_EQ,
            "A B C D");
  tt_str_op(smartlist_get(process_data->stdout_data, 3), OP_EQ,
            "");

  tt_str_op(smartlist_get(process_data->stderr_data, 0), OP_EQ,
            "Hello stderr");
  tt_str_op(smartlist_get(process_data->stderr_data, 1), OP_EQ,
            "Foo bar baz");
  tt_str_op(smartlist_get(process_data->stderr_data, 2), OP_EQ,
            "Onion Onion Onion");

 done:
  process_data_free(process_data);
  process_free(process);

  UNMOCK(process_read_stdout);
  UNMOCK(process_read_stderr);
}

static void
test_line_protocol_partial(void *arg)
{
  (void)arg;

  process_data_t *process_data = process_data_new();

  process_t *process = process_new("");
  process_set_data(process, process_data);
  process_set_stdout_read_callback(process, process_stdout_callback);
  process_set_stderr_read_callback(process, process_stderr_callback);

  MOCK(process_read_stdout, process_mocked_read_stdout);
  MOCK(process_read_stderr, process_mocked_read_stderr);

  /* Make sure we are running with the line protocol. */
  tt_int_op(PROCESS_PROTOCOL_LINE, OP_EQ, process_get_protocol(process));

  tt_int_op(0, OP_EQ, smartlist_len(process_data->stdout_data));
  tt_int_op(0, OP_EQ, smartlist_len(process_data->stderr_data));

  stdout_read_buffer = "Hello stdout this is a partial line ...";
  process_notify_event_stdout(process);
  tt_ptr_op(NULL, OP_EQ, stdout_read_buffer);

  stderr_read_buffer = "Hello stderr this is a partial line ...";
  process_notify_event_stderr(process);
  tt_ptr_op(NULL, OP_EQ, stderr_read_buffer);

  /* Data should NOT be ready. */
  tt_int_op(0, OP_EQ, smartlist_len(process_data->stdout_data));
  tt_int_op(0, OP_EQ, smartlist_len(process_data->stderr_data));

  stdout_read_buffer = " the end\nAnother partial string goes here ...";
  process_notify_event_stdout(process);
  tt_ptr_op(NULL, OP_EQ, stdout_read_buffer);

  stderr_read_buffer = " the end\nAnother partial string goes here ...";
  process_notify_event_stderr(process);
  tt_ptr_op(NULL, OP_EQ, stderr_read_buffer);

  /* Some data should be ready. */
  tt_int_op(1, OP_EQ, smartlist_len(process_data->stdout_data));
  tt_int_op(1, OP_EQ, smartlist_len(process_data->stderr_data));

  stdout_read_buffer = " the end\nFoo bar baz\n";
  process_notify_event_stdout(process);
  tt_ptr_op(NULL, OP_EQ, stdout_read_buffer);

  stderr_read_buffer = " the end\nFoo bar baz\n";
  process_notify_event_stderr(process);
  tt_ptr_op(NULL, OP_EQ, stderr_read_buffer);

  /* Some data should be ready. */
  tt_int_op(3, OP_EQ, smartlist_len(process_data->stdout_data));
  tt_int_op(3, OP_EQ, smartlist_len(process_data->stderr_data));

  /* Check if the data is correct. */
  tt_str_op(smartlist_get(process_data->stdout_data, 0), OP_EQ,
                          "Hello stdout this is a partial line ... the end");
  tt_str_op(smartlist_get(process_data->stdout_data, 1), OP_EQ,
                          "Another partial string goes here ... the end");
  tt_str_op(smartlist_get(process_data->stdout_data, 2), OP_EQ,
                          "Foo bar baz");

  tt_str_op(smartlist_get(process_data->stderr_data, 0), OP_EQ,
                          "Hello stderr this is a partial line ... the end");
  tt_str_op(smartlist_get(process_data->stderr_data, 1), OP_EQ,
                          "Another partial string goes here ... the end");
  tt_str_op(smartlist_get(process_data->stderr_data, 2), OP_EQ,
                          "Foo bar baz");

 done:
  process_data_free(process_data);
  process_free(process);

  UNMOCK(process_read_stdout);
  UNMOCK(process_read_stderr);
}

static void
test_raw_protocol_simple(void *arg)
{
  (void)arg;

  process_data_t *process_data = process_data_new();

  process_t *process = process_new("");
  process_set_data(process, process_data);
  process_set_protocol(process, PROCESS_PROTOCOL_RAW);

  process_set_stdout_read_callback(process, process_stdout_callback);
  process_set_stderr_read_callback(process, process_stderr_callback);

  MOCK(process_read_stdout, process_mocked_read_stdout);
  MOCK(process_read_stderr, process_mocked_read_stderr);

  /* Make sure we are running with the raw protocol. */
  tt_int_op(PROCESS_PROTOCOL_RAW, OP_EQ, process_get_protocol(process));

  tt_int_op(0, OP_EQ, smartlist_len(process_data->stdout_data));
  tt_int_op(0, OP_EQ, smartlist_len(process_data->stderr_data));

  stdout_read_buffer = "Hello stdout\n";
  process_notify_event_stdout(process);
  tt_ptr_op(NULL, OP_EQ, stdout_read_buffer);

  stderr_read_buffer = "Hello stderr\n";
  process_notify_event_stderr(process);
  tt_ptr_op(NULL, OP_EQ, stderr_read_buffer);

  /* Data should be ready. */
  tt_int_op(1, OP_EQ, smartlist_len(process_data->stdout_data));
  tt_int_op(1, OP_EQ, smartlist_len(process_data->stderr_data));

  stdout_read_buffer = "Hello, again, stdout\nThis contains multiple lines";
  process_notify_event_stdout(process);
  tt_ptr_op(NULL, OP_EQ, stdout_read_buffer);

  stderr_read_buffer = "Hello, again, stderr\nThis contains multiple lines";
  process_notify_event_stderr(process);
  tt_ptr_op(NULL, OP_EQ, stderr_read_buffer);

  /* Data should be ready. */
  tt_int_op(2, OP_EQ, smartlist_len(process_data->stdout_data));
  tt_int_op(2, OP_EQ, smartlist_len(process_data->stderr_data));

  /* Check if the data is correct. */
  tt_str_op(smartlist_get(process_data->stdout_data, 0), OP_EQ,
            "Hello stdout\n");
  tt_str_op(smartlist_get(process_data->stdout_data, 1), OP_EQ,
            "Hello, again, stdout\nThis contains multiple lines");

  tt_str_op(smartlist_get(process_data->stderr_data, 0), OP_EQ,
            "Hello stderr\n");
  tt_str_op(smartlist_get(process_data->stderr_data, 1), OP_EQ,
            "Hello, again, stderr\nThis contains multiple lines");

 done:
  process_data_free(process_data);
  process_free(process);

  UNMOCK(process_read_stdout);
  UNMOCK(process_read_stderr);
}

static void
test_write_simple(void *arg)
{
  (void)arg;

  process_data_t *process_data = process_data_new();

  process_t *process = process_new("");
  process_set_data(process, process_data);

  MOCK(process_write_stdin, process_mocked_write_stdin);

  process_write(process, (uint8_t *)"Hello world\n", 12);
  process_notify_event_stdin(process);
  tt_int_op(1, OP_EQ, smartlist_len(process_data->stdin_data));

  process_printf(process, "Hello %s !\n", "moon");
  process_notify_event_stdin(process);
  tt_int_op(2, OP_EQ, smartlist_len(process_data->stdin_data));

 done:
  process_data_free(process_data);
  process_free(process);

  UNMOCK(process_write_stdin);
}

static void
test_exit_simple(void *arg)
{
  (void)arg;

  process_data_t *process_data = process_data_new();

  process_t *process = process_new("");
  process_set_data(process, process_data);
  process_set_exit_callback(process, process_exit_callback);

  /* Our default is 0. */
  tt_u64_op(0, OP_EQ, process_data->exit_code);

  /* Fake that we are a running process. */
  process_set_status(process, PROCESS_STATUS_RUNNING);
  tt_int_op(process_get_status(process), OP_EQ, PROCESS_STATUS_RUNNING);

  /* Fake an exit. */
  process_notify_event_exit(process, 1337);

  /* Check if our state changed and if our callback fired. */
  tt_int_op(process_get_status(process), OP_EQ, PROCESS_STATUS_NOT_RUNNING);
  tt_u64_op(1337, OP_EQ, process_data->exit_code);

 done:
  process_set_data(process, process_data);
  process_data_free(process_data);
  process_free(process);
}

static void
test_argv_simple(void *arg)
{
  (void)arg;

  process_t *process = process_new("/bin/cat");
  char **argv = NULL;

  /* Setup some arguments. */
  process_append_argument(process, "foo");
  process_append_argument(process, "bar");
  process_append_argument(process, "baz");

  /* Check the number of elements. */
  tt_int_op(3, OP_EQ,
            smartlist_len(process_get_arguments(process)));

  /* Let's try to convert it into a Unix style char **argv. */
  argv = process_get_argv(process);

  /* Check our values. */
  tt_str_op(argv[0], OP_EQ, "/bin/cat");
  tt_str_op(argv[1], OP_EQ, "foo");
  tt_str_op(argv[2], OP_EQ, "bar");
  tt_str_op(argv[3], OP_EQ, "baz");
  tt_ptr_op(argv[4], OP_EQ, NULL);

 done:
  tor_free(argv);
  process_free(process);
}

static void
test_unix(void *arg)
{
  (void)arg;
#ifndef _WIN32
  process_t *process = process_new("");

  /* On Unix all processes should have a Unix process handle. */
  tt_ptr_op(NULL, OP_NE, process_get_unix_process(process));

 done:
  process_free(process);
#endif /* !defined(_WIN32) */
}

static void
test_win32(void *arg)
{
  (void)arg;
#ifdef _WIN32
  process_t *process = process_new("");
  char *joined_argv = NULL;

  /* On Win32 all processes should have a Win32 process handle. */
  tt_ptr_op(NULL, OP_NE, process_get_win32_process(process));

  /* Based on some test cases from "Parsing C++ Command-Line Arguments" in
   * MSDN but we don't exercise all quoting rules because tor_join_win_cmdline
   * will try to only generate simple cases for the child process to parse;
   * i.e. we never embed quoted strings in arguments. */

  const char *argvs[][4] = {
    {"a", "bb", "CCC", NULL}, // Normal
    {NULL, NULL, NULL, NULL}, // Empty argument list
    {"", NULL, NULL, NULL}, // Empty argument
    {"\"a", "b\"b", "CCC\"", NULL}, // Quotes
    {"a\tbc", "dd  dd", "E", NULL}, // Whitespace
    {"a\\\\\\b", "de fg", "H", NULL}, // Backslashes
    {"a\\\"b", "\\c", "D\\", NULL}, // Backslashes before quote
    {"a\\\\b c", "d", "E", NULL}, // Backslashes not before quote
    { NULL } // Terminator
  };

  const char *cmdlines[] = {
    "a bb CCC",
    "",
    "\"\"",
    "\\\"a b\\\"b CCC\\\"",
    "\"a\tbc\" \"dd  dd\" E",
    "a\\\\\\b \"de fg\" H",
    "a\\\\\\\"b \\c D\\",
    "\"a\\\\b c\" d E",
    NULL // Terminator
  };

  int i;

  for (i=0; cmdlines[i]!=NULL; i++) {
    log_info(LD_GENERAL, "Joining argvs[%d], expecting <%s>", i, cmdlines[i]);
    joined_argv = tor_join_win_cmdline(argvs[i]);
    tt_str_op(cmdlines[i],OP_EQ, joined_argv);
    tor_free(joined_argv);
  }

 done:
  tor_free(joined_argv);
  process_free(process);
#endif /* defined(_WIN32) */
}

struct testcase_t process_tests[] = {
  { "default_values", test_default_values, TT_FORK, NULL, NULL },
  { "environment", test_environment, TT_FORK, NULL, NULL },
  { "stringified_types", test_stringified_types, TT_FORK, NULL, NULL },
  { "line_protocol_simple", test_line_protocol_simple, TT_FORK, NULL, NULL },
  { "line_protocol_multi", test_line_protocol_multi, TT_FORK, NULL, NULL },
  { "line_protocol_partial", test_line_protocol_partial, TT_FORK, NULL, NULL },
  { "raw_protocol_simple", test_raw_protocol_simple, TT_FORK, NULL, NULL },
  { "write_simple", test_write_simple, TT_FORK, NULL, NULL },
  { "exit_simple", test_exit_simple, TT_FORK, NULL, NULL },
  { "argv_simple", test_argv_simple, TT_FORK, NULL, NULL },
  { "unix", test_unix, TT_FORK, NULL, NULL },
  { "win32", test_win32, TT_FORK, NULL, NULL },
  END_OF_TESTCASES
};
