#ifndef _GNU_SOURCE
#define _GNU_SOURCE /* CLONE_NEWUSER */
#endif

#include "libseccomp-helper.h"
#include <assert.h>
#include <errno.h>
#include <sys/socket.h>

#define LEN(arr) (sizeof(arr) / sizeof((arr)[0]))

int32_t hakurei_scmp_make_filter(
    int *ret_p, uintptr_t allocate_p,
    uint32_t arch, uint32_t multiarch,
    struct hakurei_syscall_rule *rules,
    size_t rules_sz, hakurei_export_flag flags) {
    int i;
    int last_allowed_family;
    int disallowed;
    struct hakurei_syscall_rule *rule;
    void *buf;
    size_t len = 0;

    int32_t res = 0; /* refer to resPrefix for message */

    /* Blocklist all but unix, inet, inet6 and netlink */
    struct {
        int family;
        hakurei_export_flag flags_mask;
    } socket_family_allowlist[] = {
        /* NOTE: Keep in numerical order */
        {AF_UNSPEC, 0},
        {AF_LOCAL, 0},
        {AF_INET, 0},
        {AF_INET6, 0},
        {AF_NETLINK, 0},
        {AF_CAN, HAKUREI_EXPORT_CAN},
        {AF_BLUETOOTH, HAKUREI_EXPORT_BLUETOOTH},
    };

    scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ALLOW);
    if (ctx == NULL) {
        res = 1;
        goto out;
    } else
        errno = 0;

    /* We only really need to handle arches on multiarch systems.
     * If only one arch is supported the default is fine */
    if (arch != 0) {
        /* This *adds* the target arch, instead of replacing the
         * native one. This is not ideal, because we'd like to only
         * allow the target arch, but we can't really disallow the
         * native arch at this point, because then bubblewrap
         * couldn't continue running. */
        *ret_p = seccomp_arch_add(ctx, arch);
        if (*ret_p < 0 && *ret_p != -EEXIST) {
            res = 2;
            goto out;
        }

        if (flags & HAKUREI_EXPORT_MULTIARCH && multiarch != 0) {
            *ret_p = seccomp_arch_add(ctx, multiarch);
            if (*ret_p < 0 && *ret_p != -EEXIST) {
                res = 3;
                goto out;
            }
        }
    }

    for (i = 0; i < rules_sz; i++) {
        rule = &rules[i];
        assert(rule->m_errno == EPERM || rule->m_errno == ENOSYS);

        if (rule->arg)
            *ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno), rule->syscall, 1, *rule->arg);
        else
            *ret_p = seccomp_rule_add(ctx, SCMP_ACT_ERRNO(rule->m_errno), rule->syscall, 0);

        if (*ret_p == -EFAULT) {
            res = 4;
            goto out;
        } else if (*ret_p < 0) {
            res = 5;
            goto out;
        }
    }

    /* Socket filtering doesn't work on e.g. i386, so ignore failures here
     * However, we need to user seccomp_rule_add_exact to avoid libseccomp doing
     * something else: https://github.com/seccomp/libseccomp/issues/8 */
    last_allowed_family = -1;
    for (i = 0; i < LEN(socket_family_allowlist); i++) {
        if (socket_family_allowlist[i].flags_mask != 0 &&
            (socket_family_allowlist[i].flags_mask & flags) != socket_family_allowlist[i].flags_mask)
            continue;

        for (disallowed = last_allowed_family + 1; disallowed < socket_family_allowlist[i].family; disallowed++) {
            /* Blocklist the in-between valid families */
            seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_EQ, disallowed));
        }
        last_allowed_family = socket_family_allowlist[i].family;
    }
    /* Blocklist the rest */
    seccomp_rule_add_exact(ctx, SCMP_ACT_ERRNO(EAFNOSUPPORT), SCMP_SYS(socket), 1, SCMP_A0(SCMP_CMP_GE, last_allowed_family + 1));

    if (allocate_p == 0) {
        *ret_p = seccomp_load(ctx);
        if (*ret_p != 0) {
            res = 7;
            goto out;
        }
    } else {
        *ret_p = seccomp_export_bpf_mem(ctx, NULL, &len);
        if (*ret_p != 0) {
            res = 6;
            goto out;
        }

        buf = hakurei_scmp_allocate(allocate_p, len);
        if (buf == NULL) {
            res = 4;
            goto out;
        }

        *ret_p = seccomp_export_bpf_mem(ctx, buf, &len);
        if (*ret_p != 0) {
            res = 6;
            goto out;
        }
    }

out:
    if (ctx)
        seccomp_release(ctx);

    return res;
}
