#include "sentry_alloc.h"
#include "sentry_core.h"
#include "sentry_path.h"
#include "sentry_string.h"
#include "sentry_unix_pageallocator.h"
#include "sentry_utils.h"

#ifdef SENTRY_PLATFORM_PS
#    include <sys/dirent.h>
#else
#    include <dirent.h>
#    include <libgen.h>
#endif

#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <unistd.h>

#ifdef SENTRY_PLATFORM_DARWIN
#    include <copyfile.h>
#    include <mach-o/dyld.h>
#endif

#ifdef SENTRY_PLATFORM_AIX
#    include <procinfo.h>
#endif

// only read this many bytes to memory ever
// Increased to 512MB to support large minidumps from TSAN/ASAN builds
static const size_t MAX_READ_TO_BUFFER = 536870912;

#ifndef SENTRY_PLATFORM_PS
struct sentry_pathiter_s {
    const sentry_path_t *parent;
    sentry_path_t *current;
    DIR *dir_handle;
};
#endif

static size_t
write_loop(int fd, const char *buf, size_t buf_len)
{
    while (buf_len > 0) {
        ssize_t n = write(fd, buf, buf_len);
        if (n < 0 && (errno == EAGAIN || errno == EINTR)) {
            continue;
        } else if (n <= 0) {
            break;
        }
        buf += n;
        buf_len -= n;
    }

    return buf_len;
}

bool
sentry__filelock_try_lock(sentry_filelock_t *lock)
{
#ifdef SENTRY_PLATFORM_NX
    // Nothing to do, other processes shouldn't be running at the same time.
    return true;
#endif
    lock->is_locked = false;

    int fd = open(lock->path->path, O_RDWR | O_CREAT | O_TRUNC,
        S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
    if (fd < 0) {
        return false;
    }

    if (flock(fd, LOCK_EX | LOCK_NB) != 0) {
        close(fd);
        return false;
    }

    // There is possible race between the `open` and the `flock` call, in which
    // other processes could remove the file, and create a new one with the same
    // name. So we double-check *after* having the lock, that the actual file on
    // disk is actually the one we just locked. See:
    // https://stackoverflow.com/questions/17708885/flock-removing-locked-file-without-race-condition
    struct stat st0;
    struct stat st1;
    fstat(fd, &st0);
    stat(lock->path->path, &st1);
    if (st0.st_ino != st1.st_ino) {
        close(fd);
        return false;
    }

    lock->fd = fd;
    lock->is_locked = true;
    return true;
}

void
sentry__filelock_unlock(sentry_filelock_t *lock)
{
#ifdef SENTRY_PLATFORM_NX
    // Nothing to do, see sentry__filelock_try_lock.
    return;
#endif
    if (!lock->is_locked) {
        return;
    }
    sentry__path_remove(lock->path);
    flock(lock->fd, LOCK_UN);
    close(lock->fd);
    lock->is_locked = false;
}

// Defined in a downstream SDK.
#if !defined(SENTRY_PLATFORM_NX) && !defined(SENTRY_PLATFORM_PS)
sentry_path_t *
sentry__path_absolute(const sentry_path_t *path)
{
    char full[PATH_MAX];
    if (!realpath(path->path, full)) {
        return NULL;
    }
    return sentry__path_from_str(full);
}
#endif

sentry_path_t *
sentry__path_current_exe(void)
{
#ifdef SENTRY_PLATFORM_DARWIN
    // inspired by:
    // https://github.com/rust-lang/rust/blob/0176a9eef845e7421b7e2f7ef015333a41a7c027/src/libstd/sys/unix/os.rs#L339-L358
    uint32_t buf_size = 0;
    _NSGetExecutablePath(NULL, &buf_size);
    char *buf = sentry_malloc(buf_size);
    if (!buf) {
        return NULL;
    }
    int err = _NSGetExecutablePath(buf, &buf_size);
    if (err) {
        sentry_free(buf);
        return NULL;
    }
    return sentry__path_from_str_owned(buf);
#elif defined(SENTRY_PLATFORM_LINUX)
    // inspired by:
    // https://github.com/rust-lang/rust/blob/0176a9eef845e7421b7e2f7ef015333a41a7c027/src/libstd/sys/unix/os.rs#L328-L337
    char buf[4096];
    ssize_t len = readlink("/proc/self/exe", buf, sizeof(buf) - 1);
    if (len < 0) {
        return NULL;
    }
    buf[len] = 0;
    return sentry__path_from_str(buf);
#elif defined(SENTRY_PLATFORM_AIX)
    // You can't get the full path to the current executable; the best is
    // either argv[0], or getting the name of the current executable, which
    // doesn't even include a path. Let's go with that for now.
    // (Actually, AIX may be able to under procfs, but it's System V style,
    // not like Linux. And it's not available under PASE anyways.)
    struct procentry64 proc;
    pid_t pid = getpid();
    if (getprocs64(&proc, sizeof(proc), NULL, 0, &pid, 1) < 1) {
        return NULL;
    }
    return sentry__path_from_str(proc.pi_comm);
#endif
    return NULL;
}

// Defined in a downstream SDK.
#if !defined(SENTRY_PLATFORM_PS)
sentry_path_t *
sentry__path_dir(const sentry_path_t *path)
{
    char *buf = sentry__string_clone(path->path);
    if (!buf) {
        return NULL;
    }
    // dirname can modify its argument, and may return pointers to static memory
    // that we are not allowed to free.
    char *dir = dirname(buf);
    char *newpathbuf = sentry__string_clone(dir);
    sentry_free(buf);
    if (!newpathbuf) {
        return NULL;
    }
    return sentry__path_from_str_owned(newpathbuf);
}
#endif

const char *
sentry__path_filename(const sentry_path_t *path)
{
    const char *c = strrchr(path->path, '/');
    return c ? c + 1 : path->path;
}

bool
sentry__path_filename_matches(const sentry_path_t *path, const char *filename)
{
    return sentry__string_eq(sentry__path_filename(path), filename);
}

bool
sentry__path_ends_with(const sentry_path_t *path, const char *suffix)
{
    size_t pathlen = strlen(path->path);
    size_t suffixlen = strlen(suffix);
    if (suffixlen > pathlen) {
        return false;
    }
    return sentry__string_eq(&path->path[pathlen - suffixlen], suffix);
}

bool
sentry__path_is_dir(const sentry_path_t *path)
{
    struct stat buf;
    return stat(path->path, &buf) == 0 && S_ISDIR(buf.st_mode);
}

bool
sentry__path_is_file(const sentry_path_t *path)
{
    struct stat buf;
    return stat(path->path, &buf) == 0 && S_ISREG(buf.st_mode);
}

bool
sentry__path_is_symlink(const sentry_path_t *path)
{
#if defined(SENTRY_PLATFORM_NX) || defined(SENTRY_PLATFORM_PS)
    (void)path;
    return false;
#else
    struct stat buf;
    return lstat(path->path, &buf) == 0 && S_ISLNK(buf.st_mode);
#endif
}

size_t
sentry__path_get_size(const sentry_path_t *path)
{
    struct stat buf;
    if (stat(path->path, &buf) == 0 && S_ISREG(buf.st_mode)) {
        return (size_t)buf.st_size;
    } else {
        return 0;
    }
}

time_t
sentry__path_get_mtime(const sentry_path_t *path)
{
    struct stat buf;
    if (stat(path->path, &buf) == 0) {
        return (time_t)buf.st_mtime;
    } else {
        return 0;
    }
}

sentry_path_t *
sentry__path_append_str(const sentry_path_t *base, const char *suffix)
{
    sentry_stringbuilder_t sb;

    sentry__stringbuilder_init(&sb);
    sentry__stringbuilder_append(&sb, base->path);
    sentry__stringbuilder_append(&sb, suffix);

    return sentry__path_from_str_owned(sentry__stringbuilder_into_string(&sb));
}

sentry_path_t *
sentry__path_join_str(const sentry_path_t *base, const char *other)
{
    sentry_stringbuilder_t sb;

    if (*other == '/') {
        return sentry__path_from_str(other);
    }

    sentry__stringbuilder_init(&sb);
    sentry__stringbuilder_append(&sb, base->path);

    if (!base->path[0] || base->path[strlen(base->path) - 1] != '/') {
        sentry__stringbuilder_append_char(&sb, '/');
    }
    sentry__stringbuilder_append(&sb, other);

    return sentry__path_from_str_owned(sentry__stringbuilder_into_string(&sb));
}

sentry_path_t *
sentry__path_clone(const sentry_path_t *path)
{
    sentry_path_t *rv = SENTRY_MAKE(sentry_path_t);
    if (!rv) {
        return NULL;
    }
    rv->path = sentry__string_clone(path->path);
    return rv;
}

#define EINTR_RETRY(X, Y)                                                      \
    do {                                                                       \
        int _tmp;                                                              \
        do {                                                                   \
            _tmp = (X);                                                        \
        } while (_tmp == -1 && errno == EINTR);                                \
        if (Y != 0) {                                                          \
            *(int *)Y = _tmp;                                                  \
        }                                                                      \
    } while (false)

int
sentry__path_remove(const sentry_path_t *path)
{
    int status;
    if (!sentry__path_is_dir(path) || sentry__path_is_symlink(path)) {
        EINTR_RETRY(unlink(path->path), &status);
        if (status == 0) {
            return 0;
        }
    } else {
        EINTR_RETRY(rmdir(path->path), &status);
        if (status == 0) {
            return 0;
        }
    }
    if (errno == ENOENT) {
        return 0;
    }
    return 1;
}

int
sentry__path_rename(const sentry_path_t *src, const sentry_path_t *dst)
{
    int status;
    EINTR_RETRY(rename(src->path, dst->path), &status);
    return status == 0 ? 0 : 1;
}

int
sentry__path_copy(const sentry_path_t *src, const sentry_path_t *dst)
{
#ifdef SENTRY_PLATFORM_DARWIN
    return copyfile(src->path, dst->path, NULL, COPYFILE_DATA) == 0 ? 0 : 1;
#else
    int src_fd = open(src->path, O_RDONLY);
    if (src_fd < 0) {
        return 1;
    }
    int dst_fd = open(dst->path, O_WRONLY | O_CREAT | O_TRUNC,
        S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH);
    if (dst_fd < 0) {
        close(src_fd);
        return 1;
    }

    int rv = 0;

#    ifdef SENTRY_HAVE_COPY_FILE_RANGE
    // When the page allocator is enabled, this may be running from a crash
    // handler on a constrained signal stack. Avoid copy_file_range() there:
    // it is not async-signal-safe, and lazy PLT resolution can consume enough
    // crash stack to clobber adjacent heap allocations.
    if (!sentry__page_allocator_enabled()) {
        while (true) {
            ssize_t n
                = copy_file_range(src_fd, NULL, dst_fd, NULL, SIZE_MAX / 2, 0);
            if (n > 0) {
                continue;
            } else if (n == 0) {
                goto done;
            } else if (errno == EAGAIN || errno == EINTR) {
                continue;
            } else if (errno == ENOSYS || errno == EXDEV || errno == EOPNOTSUPP
                || errno == EINVAL) {
                break;
            } else {
                rv = 1;
                goto done;
            }
        }
    }
#    endif

    {
        // Keep the fallback buffer off the stack. Crash handlers may run on
        // constrained signal stacks, so a large stack buffer is unsafe when
        // attachments are cached during crash handling.
        const size_t buf_len = 16384;
        char *buf = sentry_malloc(buf_len);
        if (!buf) {
            rv = 1;
            goto done;
        }
        while (true) {
            ssize_t n = read(src_fd, buf, buf_len);
            if (n < 0 && (errno == EAGAIN || errno == EINTR)) {
                continue;
            } else if (n <= 0) {
                rv = n < 0;
                break;
            }
            if (write_loop(dst_fd, buf, (size_t)n) != 0) {
                rv = 1;
                break;
            }
        }
        sentry_free(buf);
    }

done:
    close(src_fd);
    close(dst_fd);
    return rv;
#endif
}

int
sentry__path_create_dir_all(const sentry_path_t *path)
{
    char *p, *ptr;
    int rv = 0;
#define _TRY_MAKE_DIR                                                          \
    do {                                                                       \
        int mrv = mkdir(p, 0700);                                              \
        if (mrv != 0 && errno != EEXIST && errno != EINVAL) {                  \
            rv = 1;                                                            \
            goto done;                                                         \
        }                                                                      \
    } while (0)

    p = sentry__string_clone(path->path);
    for (ptr = p; *ptr; ptr++) {
        if (*ptr == '/' && ptr != p) {
            *ptr = 0;
            _TRY_MAKE_DIR;
            *ptr = '/';
        }
    }
    _TRY_MAKE_DIR;
#undef _TRY_MAKE_DIR

done:
    sentry_free(p);
    return rv;
}

// Defined in a downstream SDK.
#if !defined(SENTRY_PLATFORM_PS)
sentry_pathiter_t *
sentry__path_iter_directory(const sentry_path_t *path)
{
    sentry_pathiter_t *rv = SENTRY_MAKE(sentry_pathiter_t);
    if (!rv) {
        return NULL;
    }
    rv->parent = path;
    rv->current = NULL;
    rv->dir_handle = opendir(path->path);
    return rv;
}

const sentry_path_t *
sentry__pathiter_next(sentry_pathiter_t *piter)
{
    struct dirent *entry;

    if (!piter->dir_handle) {
        return NULL;
    }

    while (true) {
        entry = readdir(piter->dir_handle);
        if (!entry) {
            return NULL;
        }
        if (sentry__string_eq(entry->d_name, ".")
            || sentry__string_eq(entry->d_name, "..")) {
            continue;
        }
        break;
    }

    sentry__path_free(piter->current);
    piter->current = sentry__path_join_str(piter->parent, entry->d_name);

    return piter->current;
}

void
sentry__pathiter_free(sentry_pathiter_t *piter)
{
    if (!piter) {
        return;
    }
    if (piter->dir_handle) {
        closedir(piter->dir_handle);
    }
    sentry__path_free(piter->current);
    sentry_free(piter);
}
#endif

int
sentry__path_touch(const sentry_path_t *path)
{
    int fd = open(path->path, O_WRONLY | O_CREAT | O_APPEND,
        S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH);
    if (fd < 0) {
        return 1;
    } else {
        close(fd);
        return 0;
    }
}

char *
sentry__path_read_to_buffer(const sentry_path_t *path, size_t *size_out)
{
    int fd = open(path->path, O_RDONLY);
    if (fd < 0) {
        return NULL;
    }
    size_t len = sentry__path_get_size(path);
    if (len == 0) {
        close(fd);
        char *rv = sentry_malloc(1);
        if (rv) {
            rv[0] = '\0';
            if (size_out) {
                *size_out = 0;
            }
        }
        return rv;
    }
    if (len > MAX_READ_TO_BUFFER) {
        close(fd);
        return NULL;
    }

    // this is completely not sane in concurrent situations but hey
    char *rv = sentry_malloc(len + 1);
    if (!rv) {
        close(fd);
        return NULL;
    }

    size_t remaining = len;
    size_t offset = 0;
    while (remaining > 0) {
        ssize_t n = read(fd, rv + offset, remaining);
        if (n < 0 && (errno == EAGAIN || errno == EINTR)) {
            continue;
        } else if (n <= 0) {
            break;
        }
        offset += n;
        remaining -= n;
    }

    rv[offset] = '\0';
    close(fd);

    if (size_out) {
        *size_out = offset;
    }
    return rv;
}

static int
write_buffer_with_flags(
    const sentry_path_t *path, const char *buf, size_t buf_len, int flags)
{
    int fd = open(
        path->path, flags, S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH);
    if (fd < 0) {
        SENTRY_WARNF(
            "failed to open file \"%s\" for writing (errno %d, flags %x)",
            path->path, errno, flags);
        return 1;
    }

    size_t remaining = write_loop(fd, buf, buf_len);

    close(fd);
    return remaining == 0 ? 0 : 1;
}

int
sentry__path_write_buffer(
    const sentry_path_t *path, const char *buf, size_t buf_len)
{
    return write_buffer_with_flags(
        path, buf, buf_len, O_RDWR | O_CREAT | O_TRUNC);
}

int
sentry__path_append_buffer(
    const sentry_path_t *path, const char *buf, size_t buf_len)
{
    return write_buffer_with_flags(
        path, buf, buf_len, O_RDWR | O_CREAT | O_APPEND);
}

struct sentry_filewriter_s {
    size_t byte_count;
    int fd;
    bool failed;
    bool closed;
};

MUST_USE sentry_filewriter_t *
sentry__filewriter_new(const sentry_path_t *path)
{
    int fd = open(path->path, O_RDWR | O_CREAT | O_TRUNC,
        S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH);
    if (fd < 0) {
        return NULL;
    }

    sentry_filewriter_t *result = SENTRY_MAKE(sentry_filewriter_t);
    if (!result) {
        close(fd);
        return NULL;
    }

    result->fd = fd;
    result->byte_count = 0;
    result->failed = false;
    result->closed = false;
    return result;
}

size_t
sentry__filewriter_write(
    sentry_filewriter_t *filewriter, const char *buf, size_t buf_len)
{
    if (!filewriter || filewriter->failed || filewriter->closed) {
        return buf_len;
    }
    while (buf_len > 0) {
        ssize_t n = write(filewriter->fd, buf, buf_len);
        if (n < 0 && (errno == EAGAIN || errno == EINTR)) {
            continue;
        } else if (n <= 0) {
            filewriter->failed = true;
            break;
        }
        filewriter->byte_count += n;
        buf += n;
        buf_len -= n;
    }

    return buf_len;
}

bool
sentry__filewriter_close(sentry_filewriter_t *filewriter)
{
    if (!filewriter) {
        return false;
    }
    if (filewriter->closed) {
        return !filewriter->failed;
    }

    filewriter->closed = true;
    if (close(filewriter->fd) != 0) {
        filewriter->failed = true;
    }
    filewriter->fd = -1;
    return !filewriter->failed;
}

bool
sentry__filewriter_has_failed(const sentry_filewriter_t *filewriter)
{
    return !filewriter || filewriter->failed;
}

void
sentry__filewriter_free(sentry_filewriter_t *filewriter)
{
    if (!filewriter) {
        return;
    }

    sentry__filewriter_close(filewriter);
    sentry_free(filewriter);
}

size_t
sentry__filewriter_byte_count(const sentry_filewriter_t *filewriter)
{
    return filewriter ? filewriter->byte_count : 0;
}
