/* vi: set expandtab sw=4 sts=4: */
/* file_util.c - convenience routines for common stat operations

   Copyright (C) 2009 Ubiq Technologies <graham.gower@gmail.com>

   Carl D. Worth
   Copyright (C) 2001 University of Southern California

   SPDX-License-Identifier: GPL-2.0-or-later

   This program is free software; you can redistribute it and/or
   modify it under the terms of the GNU General Public License as
   published by the Free Software Foundation; either version 2, or (at
   your option) any later version.

   This program is distributed in the hope that it will be useful, but
   WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
   General Public License for more details.
*/

#include "config.h"

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <unistd.h>
#include <utime.h>
#include <fcntl.h>
#include <stdlib.h>
#include <string.h>

#include "opkg_message.h"
#include "opkg_archive.h"
#include "sprintf_alloc.h"
#include "file_util.h"
#include "md5.h"
#include "xfuncs.h"

#if defined HAVE_SHA256
#include "sha256.h"
#endif

/* lstat will fail if a path contains a trailing slash but is a broken symlink */
int xlstat(const char *file_name, struct stat *st)
{
    char *clean_path = NULL;
    size_t size = strlen(file_name);
    int r;

    if (size > 0 && file_name[size-1] == '/') {
        file_name = clean_path = xstrdup(file_name);
        if (!clean_path)
            return -1;
        clean_path[--size] = '\0';
    }
    r = lstat(file_name, st);
    free(clean_path);
    return r;
}

int file_exists(const char *file_name)
{
    struct stat st;
    int r;

    r = xlstat(file_name, &st);
    if (r == -1)
        return 0;

    return 1;
}

int file_is_dir(const char *file_name)
{
    struct stat st;
    int r;

    r = xlstat(file_name, &st);
    if (r == -1)
        return 0;

    return S_ISDIR(st.st_mode);
}

int file_is_symlink(const char *file_name)
{
    struct stat st;
    int r;

    r = xlstat(file_name, &st);
    if (r == -1)
        return 0;

    return S_ISLNK(st.st_mode);
}

int file_is_symlink_to_dir(const char *file_name)
{
    int is_symlink_to_dir = 0;
    if(file_is_symlink(file_name)) {
        char *link_target;
        struct stat target_stat;

        link_target = realpath(file_name, NULL);
        if (link_target) {
            is_symlink_to_dir = (xlstat(link_target, &target_stat) == 0) && S_ISDIR(target_stat.st_mode);
            free(link_target);
        }
    }
    return is_symlink_to_dir;
}

char *file_readlink_alloc(const char *file_name)
{
    struct stat st;
    char *target;
    ssize_t link_len;
    int r;

    r = xlstat(file_name, &st);
    if (r == -1)
        return NULL;

    target = malloc(st.st_size + 1);
    link_len = readlink(file_name, target, st.st_size);
    if (link_len == -1) {
        free(target);
        return NULL;
    }
    target[link_len] = 0;

    return target;
}

/* read a single line from a file, stopping at a newline or EOF.
   If a newline is read, it will appear in the resulting string.
   Return value is a malloc'ed char * which should be freed at
   some point by the caller.

   Return value is NULL if the file is at EOF when called.
*/
char *file_read_line_alloc(FILE * fp)
{
    size_t buf_len, line_size;
    char buf[BUFSIZ];
    char *line = NULL;
    int got_nl = 0;

    while (fgets(buf, BUFSIZ, fp)) {
        buf_len = strlen(buf);
        if (buf_len > 0 && buf[buf_len - 1] == '\n') {
            buf_len--;
            buf[buf_len] = '\0';
            got_nl = 1;
        }
        if (line) {
            line_size += buf_len;
            line = xrealloc(line, line_size + 1);
            strncat(line, buf, line_size);
        } else {
            line_size = buf_len + 1;
            line = xstrdup(buf);
        }
        if (got_nl)
            break;
    }

    return line;
}

static int copy_file_data(FILE * src_file, FILE * dst_file)
{
    size_t nread, nwritten;
    char buffer[BUFSIZ];

    while (1) {
        nread = fread(buffer, 1, BUFSIZ, src_file);

        if (nread != BUFSIZ && ferror(src_file)) {
            opkg_perror(ERROR, "read");
            return -1;
        }

        /* Check for EOF. */
        if (nread == 0)
            return 0;

        nwritten = fwrite(buffer, 1, nread, dst_file);

        if (nwritten != nread) {
            if (ferror(dst_file))
                opkg_perror(ERROR, "write");
            else
                opkg_msg(ERROR, "Unable to write all data.\n");
            return -1;
        }
    }
}

int file_link(const char *src, const char *dest)
{
    struct stat dest_stat;
    int r;

    r = stat(dest, &dest_stat);
    if (r == 0) {
        r = unlink(dest);
        if (r < 0) {
            opkg_perror(ERROR, "unable to remove `%s'", dest);
            return -1;
        }
    } else if (errno != ENOENT) {
        opkg_perror(ERROR, "unable to stat `%s'", dest);
        return -1;
    }

    r = symlink(src, dest);
    if (r < 0) {
        opkg_perror(DEBUG, "unable to create symlink '%s', falling back to copy",
                    dest);
        return file_copy(src, dest);
    }

    return r;
}

int file_copy(const char *src, const char *dest)
{
    struct stat src_stat;
    struct stat dest_stat;
    int dest_exists = 1;
    int status = 0;
    int r;

    r = stat(src, &src_stat);
    if (r < 0) {
        opkg_perror(ERROR, "%s", src);
        return -1;
    }

    r = stat(dest, &dest_stat);
    if (r < 0) {
        if (errno != ENOENT) {
            opkg_perror(ERROR, "unable to stat `%s'", dest);
            return -1;
        }
        dest_exists = 0;
    } else {
        int is_same_file = (src_stat.st_rdev == dest_stat.st_rdev
                && src_stat.st_ino == dest_stat.st_ino);
        if (is_same_file) {
            opkg_msg(ERROR, "`%s' and `%s' are the same file.\n", src, dest);
            return -1;
        }
    }

    if (S_ISREG(src_stat.st_mode)) {
        FILE *sfp, *dfp;
        struct utimbuf times;

        if (dest_exists) {
            dfp = fopen(dest, "w");
            if (dfp == NULL) {
                r = unlink(dest);
                if (r < 0) {
                    opkg_perror(ERROR, "unable to remove `%s'", dest);
                    return -1;
                }
            }
        } else {
            int fd;

            fd = open(dest, O_WRONLY | O_CREAT, src_stat.st_mode);
            if (fd < 0) {
                opkg_perror(ERROR, "unable to open `%s'", dest);
                return -1;
            }
            dfp = fdopen(fd, "w");
            if (dfp == NULL) {
                if (fd >= 0)
                    close(fd);
                opkg_perror(ERROR, "unable to open `%s'", dest);
                return -1;
            }
        }

        sfp = fopen(src, "r");
        if (sfp) {
            r = copy_file_data(sfp, dfp);
            if (r < 0)
                status = -1;

            r = fclose(sfp);
            if (r < 0) {
                opkg_perror(ERROR, "unable to close `%s'", src);
                status = -1;
            }
        } else {
            opkg_perror(ERROR, "unable to open `%s'", src);
            status = -1;
        }

        r = fclose(dfp);
        if (r < 0) {
            opkg_perror(ERROR, "unable to close `%s'", dest);
            status = -1;
        }

        times.actime = src_stat.st_atime;
        times.modtime = src_stat.st_mtime;
        r = utime(dest, &times);
        if (r < 0)
            opkg_perror(ERROR, "unable to preserve times of `%s'", dest);

        r = chown(dest, src_stat.st_uid, src_stat.st_gid);
        if (r < 0) {
            src_stat.st_mode &= ~(S_ISUID | S_ISGID);
            opkg_perror(ERROR, "unable to preserve ownership of `%s'", dest);
        }

        r = chmod(dest, src_stat.st_mode);
        if (r < 0)
            opkg_perror(ERROR, "unable to preserve permissions of `%s'", dest);

        return status;
    } else if (S_ISBLK(src_stat.st_mode) || S_ISCHR(src_stat.st_mode)
               || S_ISSOCK(src_stat.st_mode)) {
        r = mknod(dest, src_stat.st_mode, src_stat.st_rdev);
        if (r < 0) {
            opkg_perror(ERROR, "unable to create `%s'", dest);
            return -1;
        }
    } else if (S_ISFIFO(src_stat.st_mode)) {
        r = mkfifo(dest, src_stat.st_mode);
        if (r < 0) {
            opkg_perror(ERROR, "cannot create fifo `%s'", dest);
            return -1;
        }
    } else if (S_ISDIR(src_stat.st_mode)) {
        opkg_msg(ERROR, "%s: omitting directory.\n", src);
        return -1;
    }

    opkg_msg(ERROR, "internal error: unrecognized file type.\n");
    return -1;
}

int file_mkdir_hier(const char *path, long mode)
{
    struct stat st;
    int r;

    /* Default mode only permits user access */
    if (mode == -1) {
        mode = 0700;
    }

    r = stat(path, &st);
    if (r < 0 && errno == ENOENT) {
        int status;
        char *parent;

        parent = xdirname(path);
        status = file_mkdir_hier(parent, mode | 0300);
        free(parent);

        if (status < 0)
            return -1;

        /* mode will be further constrained by the process umask */
        r = mkdir(path, mode);
        if (r < 0) {
            opkg_perror(ERROR, "Cannot create directory `%s'", path);
            return -1;
        }

        /* Set mode again with chmod() to realize the mode specified by
         * callers, regardless of the process umask. Unlike the previous
         * mkdir() operation, chmod() ignores umask.
         */
        r = chmod(path, mode);
        if (r < 0) {
            opkg_perror(ERROR, "Cannot set permissions of directory `%s'",
                        path);
            return -1;
        }
    }

    return 0;
}

char *file_md5sum_alloc(const char *file_name)
{
    int err;
    FILE *file;
    unsigned char md5sum_bin[16];

    file = fopen(file_name, "r");
    if (file == NULL) {
        opkg_perror(ERROR, "Failed to open file %s", file_name);
        return NULL;
    }

    err = md5_stream(file, md5sum_bin);
    if (err) {
        opkg_msg(ERROR, "Could't compute md5sum for %s.\n", file_name);
        fclose(file);
        return NULL;
    }

    fclose(file);

    return md5_to_string(md5sum_bin);
}

#ifdef HAVE_SHA256
char *file_sha256sum_alloc(const char *file_name)
{
    int err;
    FILE *file;
    unsigned char sha256sum_bin[32];

    file = fopen(file_name, "r");
    if (file == NULL) {
        opkg_perror(ERROR, "Failed to open file %s", file_name);
        return NULL;
    }

    err = sha256_stream(file, sha256sum_bin);
    if (err) {
        opkg_msg(ERROR, "Could't compute sha256sum for %s.\n", file_name);
        fclose(file);
        return NULL;
    }

    fclose(file);

    return sha256_to_string(sha256sum_bin);
}

#endif

int rm_r(const char *path)
{
    int ret = 0;
    DIR *dir;
    struct dirent *dent;
    int r;

    if (path == NULL) {
        opkg_perror(ERROR, "Missing directory parameter");
        return -1;
    }

    dir = opendir(path);
    if (dir == NULL) {
        opkg_perror(ERROR, "Failed to open dir %s", path);
        return -1;
    }

    r = fchdir(dirfd(dir));
    if (r == -1) {
        opkg_perror(ERROR, "Failed to change to dir %s", path);
        closedir(dir);
        return -1;
    }

    while (1) {
        errno = 0;
        dent = readdir(dir);
        if (dent == NULL) {
            if (errno) {
                opkg_perror(ERROR, "Failed to read dir %s", path);
                ret = -1;
            }
            break;
        }

        if (!strcmp(dent->d_name, ".") || !strcmp(dent->d_name, ".."))
            continue;

#ifdef _BSD_SOURCE
        if (dent->d_type == DT_DIR) {
            ret = rm_r(dent->d_name);
            if (ret == -1)
                break;
            continue;
        } else if (dent->d_type == DT_UNKNOWN)
#endif
        {
            struct stat st;
            ret = lstat(dent->d_name, &st);
            if (ret == -1) {
                opkg_perror(ERROR, "Failed to lstat %s", dent->d_name);
                break;
            }
            if (S_ISDIR(st.st_mode)) {
                ret = rm_r(dent->d_name);
                if (ret == -1)
                    break;
                continue;
            }
        }

        ret = unlink(dent->d_name);
        if (ret == -1) {
            opkg_perror(ERROR, "Failed to unlink %s", dent->d_name);
            break;
        }
    }

    r = chdir("..");
    if (r == -1) {
        ret = -1;
        opkg_perror(ERROR, "Failed to change to dir %s/..", path);
    }

    r = rmdir(path);
    if (r == -1) {
        ret = -1;
        opkg_perror(ERROR, "Failed to remove dir %s", path);
    }

    r = closedir(dir);
    if (r == -1) {
        ret = -1;
        opkg_perror(ERROR, "Failed to close dir %s", path);
    }

    return ret;
}

int file_decompress(const char *in, const char *out)
{
    int r;
    struct opkg_ar *ar;
    FILE *f;

    ar = ar_open_compressed_file(in);
    if (!ar)
        return -1;

    /* Open output file. */
    f = fopen(out, "w");
    if (!f) {
        opkg_msg(ERROR, "Failed to open output file '%s': %s\n", out,
                 strerror(errno));
        r = -1;
        goto cleanup;
    }

    /* Copy decompressed data. */
    r = ar_copy_to_stream(ar, f);
    if (r < 0)
        goto cleanup;

    /* Success */
    r = 0;

 cleanup:
    ar_close(ar);
    if (f)
        fclose(f);

    return r;
}

int file_gz_compress(const char *in)
{
    char *in_gz;
    int r = 0;

    sprintf_alloc(&in_gz, "%s.gz", in);
    if (!gz_write_archive(in, in_gz)) {
        r = unlink(in);
        if (r != 0)
            opkg_perror(ERROR, "unable to remove `%s'", in);
    }

    free(in_gz);
    return r;
}
