/* ----------------------------------------------------------------------------
 * search path search logic
 * (c) Ben Gamari 2021
 */

#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdbool.h>

#include "common.h"

// the below is only necessary when we need to emulate execvpe.
#if !defined(HAVE_EXECVPE)

/* A quick check for whether the given path is absolute. */
static bool is_absolute(const char *path) {
    return path[0] == '/';
}

static char *concat_paths(const char *path1, const char *path2) {
    if (is_absolute(path2)) {
        return strdup(path2);
    } else {
        int len = strlen(path1) + 1 + strlen(path2) + 1;
        char *tmp = malloc(len);
        int ret = snprintf(tmp, len, "%s/%s", path1, path2);
        if (ret < 0) {
            free(tmp);
            return NULL;
        }
        return tmp;
    }
}

/* Return true if the given file exists and is an executable, optionally
 * relative to the given working directory.
 */
static bool is_executable(char *working_dir, const char *path) {
    if (working_dir && !is_absolute(path)) {
        char *tmp = concat_paths(working_dir, path);
        bool ret = access(tmp, X_OK) == 0;
        free(tmp);
        return ret;
    } else {
        return access(path, X_OK) == 0;
    }
}

/* Find an executable with the given filename in the given search path. The
 * result must be freed by the caller. Returns NULL if a matching file is not
 * found.
  */
static char *find_in_search_path(char *working_dir, char *search_path, const char *filename) {
    const int filename_len = strlen(filename);
    char *tokbuf;
    char *path = strtok_r(search_path, ":", &tokbuf);
    if (!working_dir)
        working_dir = ".";
    int workdir_len = strlen(working_dir);
    while (path != NULL) {
	// N.B. gcc 6.3.0, used by Debian 9, inexplicably warns that `path`
	// may not be initialised with -Wall.  Silence this warning. See #210.
#if defined(__GNUC__) && __GNUC__ == 6 && __GNUC_MINOR__ == 3
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
#endif
        char *tmp;
        if (is_absolute(path)) {
            const int tmp_len = strlen(path) + 1 + filename_len + 1;
            tmp = malloc(tmp_len);
            snprintf(tmp, tmp_len, "%s/%s", path, filename);
        } else {
            const int tmp_len = workdir_len + 1 + strlen(path) + 1 + filename_len + 1;
            tmp = malloc(tmp_len);
            snprintf(tmp, tmp_len, "%s/%s/%s", working_dir, path, filename);
        }
#if defined(__GNUC__) && __GNUC__ == 6 && __GNUC_MINOR__ == 3
#pragma GCC diagnostic pop
#endif

        if (is_executable(working_dir, tmp)) {
            return tmp;
        } else {
            free(tmp);
        }

        path = strtok_r(NULL, ":", &tokbuf);
    }
    return NULL;
}

/* Identify the executable search path. The result must be freed by the caller. */
static char *get_executable_search_path(void) {
    char *search_path;

    search_path = getenv("PATH");
    if (search_path) {
        search_path = strdup(search_path);
        return search_path;
    }

#if defined(HAVE_CONFSTR)
    int len = confstr(_CS_PATH, NULL, 0);
    search_path = malloc(len + 1)
    if (search_path != NULL) {
        search_path[0] = ':';
        (void) confstr (_CS_PATH, search_path + 1, len);
        return search_path;
    }
#endif

    return strdup(":");
}

/* Find the given executable in the executable search path relative to
 * workingDirectory (or the current directory, if NULL).
 * N.B. the caller is responsible for free()ing the result.
 */
char *find_executable(char *working_dir, char *filename) {
    /* Drop trailing slash from working directory if necessary */
    if (working_dir) {
        int workdir_len = strlen(working_dir);
        if (working_dir[workdir_len-1] == '/') {
            working_dir[workdir_len-1] = '\0';
        }
    }

    if (is_absolute(filename)) {
        /* If it's an absolute path name, it's easy. */
        return filename;

    } else if (strchr(filename, '/')) {
        /* If it's a relative path name, we must look for executables relative
         * to the working directory. */
        if (is_executable(working_dir, filename)) {
            return filename;
        }
    }

    /* Otherwise look through the search path... */
    char *search_path = get_executable_search_path();
    char *result = find_in_search_path(working_dir, search_path, filename);
    free(search_path);
    return result;
}

#endif
