// SPDX-License-Identifier: GPL-2.0-only
/*
 *  ppm/src/core/archive.c
 *
 *  libarchive wrapper for tar.zst extraction with sandboxing: absolute
 *  paths, "..", and symlinks that escape the destination are refused.
 *  The destination directory must be empty at extraction time.
 *
 *  Copyright (C) 2026 Project Tick
 */

#include <ppm/archive.h>

#include <archive.h>
#include <archive_entry.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

#include "../util/log.h"

/* Reject anything that would let a malicious archive escape dest_dir. */
static int entry_path_is_safe(const char *path) {
    if (!path || !*path) return 0;
    if (path[0] == '/') return 0;
    const char *p = path;
    while (*p) {
        const char *slash = strchr(p, '/');
        size_t n = slash ? (size_t)(slash - p) : strlen(p);
        if (n == 2 && p[0] == '.' && p[1] == '.') return 0;
        if (!slash) break;
        p = slash + 1;
    }
    return 1;
}

static int dir_is_empty(const char *path) {
    DIR *d = opendir(path);
    if (!d) return 0;
    struct dirent *de;
    int empty = 1;
    while ((de = readdir(d)) != NULL) {
        if (strcmp(de->d_name, ".") == 0 || strcmp(de->d_name, "..") == 0)
            continue;
        empty = 0;
        break;
    }
    closedir(d);
    return empty;
}

ppm_error_t ppm_archive_extract(const char *tarball_path, const char *dest_dir) {
    if (!tarball_path || !dest_dir) return PPM_ERR_INVALID;

    struct stat st;
    if (stat(dest_dir, &st) != 0) {
        ppm_error("archive: destination '%s' does not exist", dest_dir);
        return PPM_ERR_NOT_FOUND;
    }
    if (!S_ISDIR(st.st_mode)) {
        ppm_error("archive: destination '%s' is not a directory", dest_dir);
        return PPM_ERR_INVALID;
    }
    if (!dir_is_empty(dest_dir)) {
        ppm_error("archive: destination '%s' is not empty", dest_dir);
        return PPM_ERR_EXISTS;
    }

    struct archive *a = archive_read_new();
    if (!a) return PPM_ERR_NOMEM;
    archive_read_support_format_tar(a);
    archive_read_support_filter_zstd(a);
    archive_read_support_filter_gzip(a); /* nice-to-have for fixtures */

    struct archive *ext = archive_write_disk_new();
    if (!ext) { archive_read_free(a); return PPM_ERR_NOMEM; }

    int flags = ARCHIVE_EXTRACT_TIME
              | ARCHIVE_EXTRACT_PERM
              | ARCHIVE_EXTRACT_SECURE_NODOTDOT
              | ARCHIVE_EXTRACT_SECURE_SYMLINKS
              | ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS
              | ARCHIVE_EXTRACT_NO_OVERWRITE;
    archive_write_disk_set_options(ext, flags);
    archive_write_disk_set_standard_lookup(ext);

    if (archive_read_open_filename(a, tarball_path, 64 * 1024) != ARCHIVE_OK) {
        ppm_error("archive: cannot open '%s': %s",
                  tarball_path, archive_error_string(a));
        archive_read_free(a);
        archive_write_free(ext);
        return PPM_ERR_IO;
    }

    /* chdir into dest_dir so entries become relative to it; restore the
     * process cwd on exit. */
    int oldcwd = open(".", O_RDONLY | O_DIRECTORY | O_CLOEXEC);
    if (oldcwd < 0) {
        archive_read_free(a);
        archive_write_free(ext);
        return PPM_ERR_IO;
    }
    if (chdir(dest_dir) != 0) {
        close(oldcwd);
        archive_read_free(a);
        archive_write_free(ext);
        return PPM_ERR_IO;
    }

    ppm_error_t err = PPM_OK;
    struct archive_entry *entry;
    for (;;) {
        int r = archive_read_next_header(a, &entry);
        if (r == ARCHIVE_EOF) break;
        if (r < ARCHIVE_WARN) {
            ppm_error("archive: %s", archive_error_string(a));
            err = PPM_ERR_PARSE;
            break;
        }

        const char *pname = archive_entry_pathname(entry);
        if (!entry_path_is_safe(pname)) {
            ppm_error("archive: unsafe entry '%s' refused", pname ? pname : "(null)");
            err = PPM_ERR_INVALID;
            break;
        }

        r = archive_write_header(ext, entry);
        if (r < ARCHIVE_OK) {
            ppm_warn("archive: %s", archive_error_string(ext));
        }
        if (archive_entry_size(entry) > 0) {
            const void *buf;
            size_t size;
            la_int64_t offset;
            for (;;) {
                int rr = archive_read_data_block(a, &buf, &size, &offset);
                if (rr == ARCHIVE_EOF) break;
                if (rr < ARCHIVE_WARN) {
                    ppm_error("archive: %s", archive_error_string(a));
                    err = PPM_ERR_IO;
                    goto done;
                }
                if (archive_write_data_block(ext, buf, size, offset) < ARCHIVE_WARN) {
                    ppm_error("archive: %s", archive_error_string(ext));
                    err = PPM_ERR_IO;
                    goto done;
                }
            }
        }
        if (archive_write_finish_entry(ext) < ARCHIVE_WARN) {
            ppm_error("archive: %s", archive_error_string(ext));
            err = PPM_ERR_IO;
            break;
        }
    }

done:
    if (fchdir(oldcwd) != 0) {
        if (!err) err = PPM_ERR_IO;
    }
    close(oldcwd);

    archive_read_close(a);
    archive_read_free(a);
    archive_write_close(ext);
    archive_write_free(ext);
    return err;
}
