/*
 * miscellaneous BSD system call shims
 *
 * Copyright (c) 2013 Stacey D. Son
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#ifndef BSD_MISC_H
#define BSD_MISC_H

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/sem.h>
#include <sys/uuid.h>

#include "qemu-bsd.h"

static int bsd_msgmax;

/* quotactl(2) */
static inline abi_long do_bsd_quotactl(abi_ulong path, abi_long cmd,
        __unused abi_ulong target_addr)
{
    qemu_log("qemu: Unsupported syscall quotactl()\n");
    return -TARGET_ENOSYS;
}

/* reboot(2) */
static inline abi_long do_bsd_reboot(abi_long how)
{
    qemu_log("qemu: Unsupported syscall reboot()\n");
    return -TARGET_ENOSYS;
}

/* uuidgen(2) */
static inline abi_long do_bsd_uuidgen(abi_ulong target_addr, int count)
{
    int i;
    abi_long ret;
    g_autofree struct uuid *host_uuid = NULL;

    /*
     * 2048 is the kernel limit, but there's no #define for it, nor any sysctl
     * to query it.
     */
    if (count < 1 || count > 2048) {
        return -TARGET_EINVAL;
    }

    host_uuid = g_malloc(count * sizeof(struct uuid));

    ret = get_errno(uuidgen(host_uuid, count));
    if (is_error(ret)) {
        goto out;
    }
    for (i = 0; i < count; i++) {
        ret = host_to_target_uuid(target_addr +
            (abi_ulong)(sizeof(struct target_uuid) * i), &host_uuid[i]);
        if (is_error(ret)) {
            break;
        }
    }

out:
    return ret;
}

/*
 * System V Semaphores
 */

/* semget(2) */
static inline abi_long do_bsd_semget(abi_long key, int nsems,
        int target_flags)
{
    return get_errno(semget(key, nsems,
                target_to_host_bitmask(target_flags, ipc_flags_tbl)));
}

/* semop(2) */
static inline abi_long do_bsd_semop(int semid, abi_long ptr, unsigned nsops)
{
    g_autofree struct sembuf *sops = g_malloc(nsops * sizeof(struct sembuf));
    struct target_sembuf *target_sembuf;
    int i;

    target_sembuf = lock_user(VERIFY_READ, ptr,
            nsops * sizeof(struct target_sembuf), 1);
    if (target_sembuf == NULL) {
        return -TARGET_EFAULT;
    }
    for (i = 0; i < nsops; i++) {
        __get_user(sops[i].sem_num, &target_sembuf[i].sem_num);
        __get_user(sops[i].sem_op, &target_sembuf[i].sem_op);
        __get_user(sops[i].sem_flg, &target_sembuf[i].sem_flg);
    }
    unlock_user(target_sembuf, ptr, 0);

    return semop(semid, sops, nsops);
}

/* __semctl(2) */
static inline abi_long do_bsd___semctl(int semid, int semnum, int target_cmd,
                                       abi_ptr un_ptr)
{
    void *target_un;
    union semun arg;
    struct semid_ds dsarg;
    unsigned short *array = NULL;
    int host_cmd;
    abi_long ret = 0;
    abi_ulong target_array, target_buffer;

    switch (target_cmd) {
    case TARGET_GETVAL:
        host_cmd = GETVAL;
        break;

    case TARGET_SETVAL:
        host_cmd = SETVAL;
        break;

    case TARGET_GETALL:
        host_cmd = GETALL;
        break;

    case TARGET_SETALL:
        host_cmd = SETALL;
        break;

    case TARGET_IPC_STAT:
        host_cmd = IPC_STAT;
        break;

    case TARGET_IPC_SET:
        host_cmd = IPC_SET;
        break;

    case TARGET_IPC_RMID:
        host_cmd = IPC_RMID;
        break;

    case TARGET_GETPID:
        host_cmd = GETPID;
        break;

    case TARGET_GETNCNT:
        host_cmd = GETNCNT;
        break;

    case TARGET_GETZCNT:
        host_cmd = GETZCNT;
        break;

    default:
        return -TARGET_EINVAL;
    }

    /*
     * Unlike Linux and the semctl system call, we take a pointer
     * to the union arg here.
     */
    target_un = lock_user(VERIFY_READ, un_ptr, sizeof(union target_semun), 1);

    switch (host_cmd) {
    case GETVAL:
    case SETVAL:
        __get_user(arg.val, (abi_int *)target_un);
        ret = get_errno(semctl(semid, semnum, host_cmd, arg));
        break;

    case GETALL:
    case SETALL:
        __get_user(target_array, (abi_ulong *)target_un);
        ret = target_to_host_semarray(semid, &array, target_array);
        if (is_error(ret)) {
            goto out;
        }
        arg.array = array;
        ret = get_errno(semctl(semid, semnum, host_cmd, arg));
        if (!is_error(ret)) {
            ret = host_to_target_semarray(semid, target_array, &array);
        }
        break;

    case IPC_STAT:
    case IPC_SET:
        __get_user(target_buffer, (abi_ulong *)target_un);
        ret = target_to_host_semid_ds(&dsarg, target_buffer);
        if (is_error(ret)) {
            goto out;
        }
        arg.buf = &dsarg;
        ret = get_errno(semctl(semid, semnum, host_cmd, arg));
        if (!is_error(ret)) {
            ret = host_to_target_semid_ds(target_buffer, &dsarg);
        }
        break;

    case IPC_RMID:
    case GETPID:
    case GETNCNT:
    case GETZCNT:
        ret = get_errno(semctl(semid, semnum, host_cmd, NULL));
        break;

    default:
        ret = -TARGET_EINVAL;
        break;
    }
out:
    unlock_user(target_un, un_ptr, 1);
    return ret;
}

/* msgctl(2) */
static inline abi_long do_bsd_msgctl(int msgid, int target_cmd, abi_long ptr)
{
    struct msqid_ds dsarg;
    abi_long ret = -TARGET_EINVAL;
    int host_cmd;

    switch (target_cmd) {
    case TARGET_IPC_STAT:
        host_cmd = IPC_STAT;
        break;

    case TARGET_IPC_SET:
        host_cmd = IPC_SET;
        break;

    case TARGET_IPC_RMID:
        host_cmd = IPC_RMID;
        break;

    default:
        return -TARGET_EINVAL;
    }

    switch (host_cmd) {
    case IPC_STAT:
    case IPC_SET:
        if (target_to_host_msqid_ds(&dsarg, ptr)) {
            return -TARGET_EFAULT;
        }
        ret = get_errno(msgctl(msgid, host_cmd, &dsarg));
        if (host_to_target_msqid_ds(ptr, &dsarg)) {
            return -TARGET_EFAULT;
        }
        break;

    case IPC_RMID:
        ret = get_errno(msgctl(msgid, host_cmd, NULL));
        break;

    default:
        ret = -TARGET_EINVAL;
        break;
    }
    return ret;
}

struct kern_mymsg {
    long mtype;
    char mtext[1];
};

static inline abi_long bsd_validate_msgsz(abi_ulong msgsz)
{
    /* Fetch msgmax the first time we need it. */
    if (bsd_msgmax == 0) {
        size_t len = sizeof(bsd_msgmax);

        if (sysctlbyname("kern.ipc.msgmax", &bsd_msgmax, &len, NULL, 0) == -1) {
            return -TARGET_EINVAL;
        }
    }

    if (msgsz > bsd_msgmax) {
        return -TARGET_EINVAL;
    }
    return 0;
}

/* msgsnd(2) */
static inline abi_long do_bsd_msgsnd(int msqid, abi_long msgp,
        abi_ulong msgsz, int msgflg)
{
    struct target_msgbuf *target_mb;
    struct kern_mymsg *host_mb;
    abi_long ret;

    ret = bsd_validate_msgsz(msgsz);
    if (is_error(ret)) {
        return ret;
    }
    if (!lock_user_struct(VERIFY_READ, target_mb, msgp, 0)) {
        return -TARGET_EFAULT;
    }
    host_mb = g_malloc(msgsz + sizeof(long));
    host_mb->mtype = (abi_long) tswapal(target_mb->mtype);
    memcpy(host_mb->mtext, target_mb->mtext, msgsz);
    ret = get_errno(msgsnd(msqid, host_mb, msgsz, msgflg));
    g_free(host_mb);
    unlock_user_struct(target_mb, msgp, 0);

    return ret;
}

/* msgget(2) */
static inline abi_long do_bsd_msgget(abi_long key, abi_long msgflag)
{
    abi_long ret;

    ret = get_errno(msgget(key, msgflag));
    return ret;
}

/* msgrcv(2) */
static inline abi_long do_bsd_msgrcv(int msqid, abi_long msgp,
        abi_ulong msgsz, abi_long msgtyp, int msgflg)
{
    struct target_msgbuf *target_mb = NULL;
    char *target_mtext;
    struct kern_mymsg *host_mb;
    abi_long ret = 0;

    ret = bsd_validate_msgsz(msgsz);
    if (is_error(ret)) {
        return ret;
    }
    if (!lock_user_struct(VERIFY_WRITE, target_mb, msgp, 0)) {
        return -TARGET_EFAULT;
    }
    host_mb = g_malloc(msgsz + sizeof(long));
    ret = get_errno(msgrcv(msqid, host_mb, msgsz, tswapal(msgtyp), msgflg));
    if (ret > 0) {
        abi_ulong target_mtext_addr = msgp + sizeof(abi_ulong);
        target_mtext = lock_user(VERIFY_WRITE, target_mtext_addr, ret, 0);
        if (target_mtext == NULL) {
            ret = -TARGET_EFAULT;
            goto end;
        }
        memcpy(target_mb->mtext, host_mb->mtext, ret);
        unlock_user(target_mtext, target_mtext_addr, ret);
    }
    if (!is_error(ret)) {
        target_mb->mtype = tswapal(host_mb->mtype);
    }
end:
    if (target_mb != NULL) {
        unlock_user_struct(target_mb, msgp, 1);
    }
    g_free(host_mb);
    return ret;
}

/* getdtablesize(2) */
static inline abi_long do_bsd_getdtablesize(void)
{
    return get_errno(getdtablesize());
}

#endif /* BSD_MISC_H */
