/* ========================================================================= */
/**
 * @file file.c
 *
 * @copyright
 * Copyright 2023 Google LLC
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <errno.h>
#include <fcntl.h>
#include <fnmatch.h>
#include <fts.h>
#include <libbase/file.h>
#include <libbase/log.h>
#include <libbase/strutil.h>
#include <libbase/test.h>
#include <libgen.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include "libbase/log_wrappers.h"

/* == Declarations ========================================================= */

#if defined __linux__
static int _bs_file_walk_compare(
    const FTSENT **a,
    const FTSENT **b);
#else
static int _bs_file_walk_compare(
    const FTSENT * const *a,
    const FTSENT * const *b);
#endif

/* == Exported methods ===================================================== */

/* ------------------------------------------------------------------------- */
ssize_t bs_file_read_buffer(
    const char *fname_ptr,
    char *buf_ptr,
    size_t buf_len)
{
    ssize_t                   read_bytes;
    int                       fd;

    fd = open(fname_ptr, O_RDONLY);
    if (0 > fd) {
        bs_log(BS_WARNING | BS_ERRNO, "Failed open(%s, O_RDONLY)",
               fname_ptr);
        return -1;
    }

    read_bytes = read(fd, buf_ptr, buf_len);
    if (0 > read_bytes) {
        bs_log(BS_WARNING | BS_ERRNO, "Failed read(%d, %p, %zu) from %s",
               fd, buf_ptr, buf_len - 1, fname_ptr);
    } if ((size_t)read_bytes >= buf_len) {
        bs_log(BS_WARNING | BS_ERRNO,
               "Read %zd >= %zu bytes. Too much data in %s",
               read_bytes, buf_len, fname_ptr);
        read_bytes = -1;
    } else {
        buf_ptr[read_bytes] = '\0';
    }

    if (0 != close(fd)) {
        bs_log(BS_WARNING | BS_ERRNO, "Failed close(%d) for %s", fd,
               fname_ptr);
        return -1;
    }

    return read_bytes;
}

/* ------------------------------------------------------------------------- */
ssize_t bs_file_write_buffer(
    const char *fname_ptr,
    const char *buf_ptr,
    size_t buf_len)
{
    ssize_t                   written_bytes;
    int                       fd;

    fd = open(fname_ptr, O_WRONLY);
    if (0 > fd) {
        bs_log(BS_WARNING | BS_ERRNO, "Failed open(%s, O_WRONLY)", fname_ptr);
        return -1;
    }

    written_bytes = write(fd, buf_ptr, buf_len);
    if (0 > written_bytes) {
        bs_log(BS_ERROR | BS_ERRNO, "Falied write(%d, %p, %zu) to %s",
               fd, buf_ptr, buf_len, fname_ptr);
    } else if ((size_t)written_bytes != buf_len) {
        bs_log(BS_ERROR, "Incomplete write(%d, %p, %zu): %zd bytes to %s",
               fd, buf_ptr, buf_len, written_bytes, fname_ptr);
        written_bytes = -1;
    }

    if (0 != close(fd)) {
        bs_log(BS_WARNING | BS_ERRNO, "Failed close(%d) for %s", fd,
               fname_ptr);
        return -1;
    }

    return written_bytes;
}

/* ------------------------------------------------------------------------- */
char *bs_file_resolve_path(
    const char *path_ptr,
    char *resolved_path_buf_ptr)
{
    char expanded_path[PATH_MAX];
    if (bs_str_startswith(path_ptr, "~/")) {
        const char *home_dir_ptr = getenv("HOME");
        if (NULL == home_dir_ptr) {
            bs_log(BS_WARNING, "Failed getenv(\"HOME\") for path %s",
                   path_ptr);
        } else {
            size_t i = bs_strappend(expanded_path, PATH_MAX, 0, home_dir_ptr);
            i = bs_strappend(expanded_path, PATH_MAX, i, "/");
            i = bs_strappend(expanded_path, PATH_MAX, i, path_ptr + 2);
            if (i >= PATH_MAX) {
                errno = ENAMETOOLONG;
                return NULL;
            }
            path_ptr = &expanded_path[0];
        }
    }

    return realpath(path_ptr, resolved_path_buf_ptr);
}

/* ------------------------------------------------------------------------- */
char *bs_file_join_resolve_path(
    const char *path_ptr,
    const char *fname_ptr,
    char *resolved_path_buf_ptr)
{
    char path[PATH_MAX];
    size_t pos = bs_strappend(path, PATH_MAX, 0, path_ptr);
    pos = bs_strappend(path, PATH_MAX, pos, "/");
    pos = bs_strappend(path, PATH_MAX, pos, fname_ptr);
    if (pos > PATH_MAX) {
        errno = ENAMETOOLONG;
        return NULL;
    }

    return bs_file_resolve_path(path, resolved_path_buf_ptr);
}

/* ------------------------------------------------------------------------- */
char *bs_file_resolve_and_lookup_from_paths(
    const char *fname_ptr,
    const char **paths_ptr_ptr,
    int mode,
    char *resolved_path_buf_ptr)
{
    for (; NULL != *paths_ptr_ptr; ++paths_ptr_ptr) {
        char *resolved_path_ptr = bs_file_join_resolve_path(
            *paths_ptr_ptr, fname_ptr, resolved_path_buf_ptr);
        if (NULL == resolved_path_ptr) continue;

        // Found something and not needed to check type? We have it.
        if (0 == mode) return resolved_path_ptr;

        // Otherwise, verify type.
        struct stat stat_buf;
        if (0 == stat(resolved_path_ptr, &stat_buf)) {
            if ((stat_buf.st_mode & S_IFMT) == (mode & S_IFMT)) {
                return resolved_path_ptr;
            }
        }
        if (NULL == resolved_path_buf_ptr) free(resolved_path_ptr);
    }
    return NULL;
}

/* ------------------------------------------------------------------------- */
bool bs_file_mkdir_p(const char *dirname_ptr, int mode)
{
    if (0 == mkdir(dirname_ptr, mode)) return true;

    if (ENOENT != errno) {
        bs_log(BS_ERROR | BS_ERRNO, "Failed mkdir(\"%s\", %o)",
               dirname_ptr, mode);
        return false;
    }

    // A parent does not exist. Attempt to create the parent, then try again.
    char *parent_dirname_ptr = logged_strdup(dirname_ptr);
    if (NULL == parent_dirname_ptr) return false;
    bool rv = bs_file_mkdir_p(dirname(parent_dirname_ptr), mode);
    free(parent_dirname_ptr);
    if (!rv) return false;

    return 0 == mkdir(dirname_ptr, mode);
}

/* ------------------------------------------------------------------------- */
bool bs_file_realpath_is(const char *fname_ptr, int mode_type)
{
    char *resolved_fname_ptr = realpath(fname_ptr, NULL);
    if (NULL == resolved_fname_ptr) {
        if (ENOENT != errno) {
            bs_log(BS_ERROR, "Failed realpath(\"%s\", NULL)", fname_ptr);
        }
        return false;
    }

    struct stat stat_buf;
    bool rv = (0 == stat(resolved_fname_ptr, &stat_buf));
    if (rv) {
        rv = ((stat_buf.st_mode & S_IFMT) == (mode_t)mode_type);
    } else if (ENOENT != errno) {
        bs_log(BS_ERROR, "Failed stat(\"%s\", %p)",
               resolved_fname_ptr, &stat_buf);
    }

    free(resolved_fname_ptr);
    return rv;
}

/* ------------------------------------------------------------------------- */
bool bs_file_walk_tree(
    const char *path_ptr,
    const char *pattern_ptr,
    int mode_type,
    bool sorted,
    bool (*callback)(const char *, const FTSENT *, void *),
    void *ud_ptr)
{
    char *const paths[2] = { (char*)path_ptr, NULL };

    FTS *fts_ptr = fts_open(
        paths,
        FTS_PHYSICAL,
        sorted ? _bs_file_walk_compare : NULL);
    if (NULL == fts_ptr) {
        bs_log(BS_ERROR | BS_ERRNO,
               "Failed fts_open({\"%s\", NULL}, FTS_PHYSICAL, NULL)",
               path_ptr);
        return false;
    }

    FTSENT *ftsent_ptr = NULL;
    bool rv = true;
    while (NULL != (ftsent_ptr = fts_read(fts_ptr))) {

        // Restrict to files & directories. Omit the FTS_DP post-pass, as it
        // would make directories show up twice.
        if (ftsent_ptr->fts_info != FTS_D &&
            ftsent_ptr->fts_info != FTS_DEFAULT &&
            ftsent_ptr->fts_info != FTS_SL &&
            ftsent_ptr->fts_info != FTS_F) continue;

        // Restrict to requested mode, if given.
        if (0 != mode_type &&
            (ftsent_ptr->fts_statp->st_mode & S_IFMT) != (mode_t)mode_type) {
            continue;
        }

        // Optional pattern match.
        if (NULL != pattern_ptr &&
            0 != fnmatch(pattern_ptr, ftsent_ptr->fts_name, 0)) continue;

        char *resolved_path_ptr = realpath(ftsent_ptr->fts_accpath, NULL);
        if (NULL == resolved_path_ptr) {
            bs_log(BS_ERROR | BS_ERRNO, "Failed realpath(\"%s\", NULL)",
                   ftsent_ptr->fts_accpath);
            rv = false;
            break;
        }
        rv &= callback(resolved_path_ptr, ftsent_ptr, ud_ptr);
        free(resolved_path_ptr);

        if (!rv) break;
    }

    fts_close(fts_ptr);
    return rv;
}

/* == Static (local) functions ============================================= */

/** Comparator to fts_open() (see fts(3)). Used when sorting is requested. */
#if defined __linux__
static int _bs_file_walk_compare(
    const FTSENT **a,
    const FTSENT **b)
#else
static int _bs_file_walk_compare(
    const FTSENT * const *a,
    const FTSENT * const *b)
#endif
{
    return strcmp((*a)->fts_name, (*b)->fts_name);
}

/* == Test Functions ======================================================= */

static void test_resolve_path(bs_test_t *test_ptr);
static void test_join_resolve_path(bs_test_t *test_ptr);
static void test_lookup(bs_test_t *test_ptr);
static void test_mkdir_p(bs_test_t *test_ptr);
static void test_realpath_is(bs_test_t *test_ptr);
static void test_walk_tree(bs_test_t *test_ptr);

/** Unit test cases. */
static const bs_test_case_t bs_file_test_cases[] = {
    { true, "resolve_path", test_resolve_path },
    { true, "join_resolve_path", test_join_resolve_path },
    { true, "lookup", test_lookup },
    { true, "mkdir_p", test_mkdir_p },
    { true, "realpath_is", test_realpath_is },
    { true, "walk_tree", test_walk_tree },
    { false, NULL, NULL }  // sentinel.
};

const bs_test_set_t bs_file_test_set = BS_TEST_SET(
    true, "file", bs_file_test_cases);

/* ------------------------------------------------------------------------- */
void test_resolve_path(bs_test_t *test_ptr)
{
    char *p;

    p = bs_file_resolve_path("/etc/../etc/passwd", NULL);
    BS_TEST_VERIFY_STREQ(test_ptr, "/etc/passwd", p);
    free(p);

    char path[PATH_MAX];
    p = bs_file_resolve_path("/etc/../etc/passwd", path);
    BS_TEST_VERIFY_STREQ(test_ptr, "/etc/passwd", p);
    BS_TEST_VERIFY_EQ(test_ptr, p, &path[0]);

    p = bs_file_resolve_path("~/", path);
    BS_TEST_VERIFY_NEQ(test_ptr, NULL, p);
}

/* ------------------------------------------------------------------------- */
void test_join_resolve_path(bs_test_t *test_ptr)
{
    char *p;
    p = bs_file_join_resolve_path("/etc/../etc", "passwd", NULL);
    BS_TEST_VERIFY_NEQ(test_ptr, NULL, p);
    free(p);
}

/* ------------------------------------------------------------------------- */
void test_lookup(bs_test_t *test_ptr)
{
    const char *paths[] = { NULL, NULL, NULL };
    char path[PATH_MAX];
    char *p;

#if defined(__LIBBASE_LINUX)
    // The '/proc/self' moniker works only on Linux.

    paths[0] = "/anywhere";

    char self_path[PATH_MAX];
    p = realpath("/proc/self/exe", self_path);
    BS_TEST_VERIFY_NEQ(test_ptr, NULL, p);
    paths[1] = dirname(p);

    p = bs_file_resolve_and_lookup_from_paths(
        "libbase_test", paths, 0, NULL);
    BS_TEST_VERIFY_NEQ(test_ptr, NULL, p);
    free(p);

    p = bs_file_resolve_and_lookup_from_paths(
        "libbase_test", paths, S_IFBLK, NULL);
    BS_TEST_VERIFY_EQ(test_ptr, NULL, p);

    p = bs_file_resolve_and_lookup_from_paths(
        "does_not_exist", paths, 0, NULL);
    BS_TEST_VERIFY_EQ(test_ptr, NULL, p);

    // Lookup into provided |path| arg.
    p = bs_file_resolve_and_lookup_from_paths(
        "libbase_test", paths, 0, path);
    BS_TEST_VERIFY_NEQ(test_ptr, NULL, p);

    p = bs_file_resolve_and_lookup_from_paths(
        "libbase_test", paths, S_IFREG, path);
    BS_TEST_VERIFY_NEQ(test_ptr, NULL, p);
#endif  // __LIBBASE_LINUX

    paths[0] = "~/";
    paths[1] = NULL;
    p = bs_file_resolve_and_lookup_from_paths(
        "", paths, S_IFDIR, path);
    BS_TEST_VERIFY_NEQ(test_ptr, NULL, p);
}

/* ------------------------------------------------------------------------- */
void test_mkdir_p(bs_test_t *test_ptr)
{
    const char *dirname_ptr = bs_test_temp_path(test_ptr, "a/b");
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, dirname_ptr);

    // The temp dir was just created: First attempt must succeed.
    BS_TEST_VERIFY_TRUE(test_ptr, bs_file_mkdir_p(dirname_ptr, 0700));

    // Second attempt already finds the directories, must fail.
    BS_TEST_VERIFY_FALSE(test_ptr, bs_file_mkdir_p(dirname_ptr, 0700));

    rmdir(dirname_ptr);
    rmdir(bs_test_temp_path(test_ptr, "a"));
}

/* ------------------------------------------------------------------------- */
void test_realpath_is(bs_test_t *test_ptr)
{
    const char *p = bs_test_temp_path(test_ptr, NULL);
    BS_TEST_VERIFY_TRUE(test_ptr, bs_file_realpath_is(p, S_IFDIR));
    BS_TEST_VERIFY_FALSE(test_ptr, bs_file_realpath_is(p, S_IFREG));

    char *fn = bs_strdupf("%s/a", p);
    BS_TEST_VERIFY_NEQ_OR_RETURN(test_ptr, NULL, fn);
    BS_TEST_VERIFY_FALSE(test_ptr, bs_file_realpath_is(fn, S_IFDIR));
    BS_TEST_VERIFY_FALSE(test_ptr, bs_file_realpath_is(fn, S_IFREG));

    int fd = creat(fn, 0600);
    close(fd);
    BS_TEST_VERIFY_TRUE(test_ptr, bs_file_realpath_is(fn, S_IFREG));

    unlink(fn);
    free(fn);
}

/* ------------------------------------------------------------------------- */
/** Argument to @ref _test_walk_cb. */
struct _test_walk_arg {
    /** Number of calls. */
    size_t count;
    /** Unit test. */
    bs_test_t *test_ptr;
};

/** Mirrors sorted contents of all plist files in the test directory. */
static const char* const _test_walk_files[] = {
    "array.plist", "dict.plist", "string.plist" };

/** Callback for @ref bs_file_walk_tree. */
bool _test_walk_cb(const char *path_ptr,
                   const FTSENT *ftsent_ptr,
                   void *ud_ptr)
{
    struct _test_walk_arg *arg_ptr = ud_ptr;

    if (arg_ptr->count >= (sizeof(_test_walk_files) / sizeof(char*))) {
        return false;
    }
    int rv = strcmp(_test_walk_files[arg_ptr->count], ftsent_ptr->fts_name);
    if (rv) {
        BS_TEST_FAIL(arg_ptr->test_ptr,
                     "Expected file \"%s\", found \"%s\" at %zu",
                     path_ptr, ftsent_ptr->fts_name, arg_ptr->count);
    }

    ++arg_ptr->count;
    return 0 == rv;
}

/** Tests @ref bs_file_walk_tree. */
void test_walk_tree(bs_test_t *test_ptr)
{
    struct _test_walk_arg arg = { .test_ptr = test_ptr };
    BS_TEST_VERIFY_TRUE(
        test_ptr,
        bs_file_walk_tree(bs_test_data_path(test_ptr, "/"),
                          "*.plist",
                          0,  // no particular mode.
                          true,
                          _test_walk_cb,
                          &arg));
    BS_TEST_VERIFY_EQ(
        test_ptr, sizeof(_test_walk_files) / sizeof(char*), arg.count);
}

/* == End of file.c ======================================================== */
