/*
 * Copyright 2024-2025 The OpenSSL Project Authors. All Rights Reserved.
 *
 * Licensed under the Apache License 2.0 (the "License").  You may not use
 * this file except in compliance with the License.  You can obtain a copy
 * in the file LICENSE in the source distribution or at
 * https://www.openssl.org/source/license.html
 */

#include <assert.h>
#include <errno.h>
#include "internal/safe_math.h"
#include "poll_builder.h"

OSSL_SAFE_MATH_UNSIGNED(size_t, size_t)

int ossl_rio_poll_builder_init(RIO_POLL_BUILDER *rpb)
{
#if RIO_POLL_METHOD == RIO_POLL_METHOD_NONE
    return 0;
#elif RIO_POLL_METHOD == RIO_POLL_METHOD_SELECT
    FD_ZERO(&rpb->rfd);
    FD_ZERO(&rpb->wfd);
    FD_ZERO(&rpb->efd);
    rpb->hwm_fd = -1;
#elif RIO_POLL_METHOD == RIO_POLL_METHOD_POLL
    rpb->pfd_heap = NULL;
    rpb->pfd_num = 0;
    rpb->pfd_alloc = OSSL_NELEM(rpb->pfds);
#endif
    return 1;
}

void ossl_rio_poll_builder_cleanup(RIO_POLL_BUILDER *rpb)
{
    if (rpb == NULL)
        return;

#if RIO_POLL_METHOD == RIO_POLL_METHOD_POLL
    OPENSSL_free(rpb->pfd_heap);
#endif
}

#if RIO_POLL_METHOD == RIO_POLL_METHOD_POLL
static int rpb_ensure_alloc(RIO_POLL_BUILDER *rpb, size_t alloc)
{
    struct pollfd *pfd_heap_new;
    size_t total_size;
    int error = 0;

    if (alloc <= rpb->pfd_alloc)
        return 1;

    total_size = safe_mul_size_t(alloc, sizeof(struct pollfd), &error);
    if (error)
        return 0;

    pfd_heap_new = OPENSSL_realloc(rpb->pfd_heap, total_size);
    if (pfd_heap_new == NULL)
        return 0;

    if (rpb->pfd_heap == NULL) {
        /* Copy the contents of the stacked array. */
        memcpy(pfd_heap_new, rpb->pfds, sizeof(rpb->pfds));
    }
    rpb->pfd_heap = pfd_heap_new;
    rpb->pfd_alloc = alloc;
    return 1;
}
#endif

int ossl_rio_poll_builder_add_fd(RIO_POLL_BUILDER *rpb, int fd,
    int want_read, int want_write)
{
#if RIO_POLL_METHOD == RIO_POLL_METHOD_POLL
    size_t i;
    struct pollfd *pfds = (rpb->pfd_heap != NULL ? rpb->pfd_heap : rpb->pfds);
    struct pollfd *pfd;
#endif

    if (fd < 0)
        return 0;

#if RIO_POLL_METHOD == RIO_POLL_METHOD_SELECT

#ifndef OPENSSL_SYS_WINDOWS
    /*
     * On Windows there is no relevant limit to the magnitude of a fd value (see
     * above). On *NIX the fd_set uses a bitmap and we must check the limit.
     */
    if (fd >= FD_SETSIZE)
        return 0;
#endif

    if (want_read)
        openssl_fdset(fd, &rpb->rfd);

    if (want_write)
        openssl_fdset(fd, &rpb->wfd);

    openssl_fdset(fd, &rpb->efd);
    if (fd > rpb->hwm_fd)
        rpb->hwm_fd = fd;

    return 1;

#elif RIO_POLL_METHOD == RIO_POLL_METHOD_POLL
    for (i = 0; i < rpb->pfd_num; ++i) {
        pfd = &pfds[i];

        if (pfd->fd == -1 || pfd->fd == fd)
            break;
    }

    if (i >= rpb->pfd_alloc) {
        if (!rpb_ensure_alloc(rpb, rpb->pfd_alloc * 2))
            return 0;
        pfds = rpb->pfd_heap;
    }

    assert((rpb->pfd_heap != NULL && rpb->pfd_heap == pfds) || (rpb->pfd_heap == NULL && rpb->pfds == pfds));
    assert(i <= rpb->pfd_num && rpb->pfd_num <= rpb->pfd_alloc);
    pfds[i].fd = fd;
    pfds[i].events = 0;

    if (want_read)
        pfds[i].events |= POLLIN;
    if (want_write)
        pfds[i].events |= POLLOUT;

    if (i == rpb->pfd_num)
        ++rpb->pfd_num;

    return 1;
#endif
}

int ossl_rio_poll_builder_poll(RIO_POLL_BUILDER *rpb, OSSL_TIME deadline)
{
    int rc;

#if RIO_POLL_METHOD == RIO_POLL_METHOD_SELECT
    do {
        struct timeval timeout, *p_timeout = &timeout;

        /*
         * select expects a timeout, not a deadline, so do the conversion.
         * Update for each call to ensure the correct value is used if we repeat
         * due to EINTR.
         */
        if (ossl_time_is_infinite(deadline))
            p_timeout = NULL;
        else
            /*
             * ossl_time_subtract saturates to zero so we don't need to check if
             * now > deadline.
             */
            timeout = ossl_time_to_timeval(ossl_time_subtract(deadline,
                ossl_time_now()));

        rc = select(rpb->hwm_fd + 1, &rpb->rfd, &rpb->wfd, &rpb->efd, p_timeout);
    } while (rc == -1 && get_last_socket_error_is_eintr());
#elif RIO_POLL_METHOD == RIO_POLL_METHOD_POLL
    do {
        int timeout_ms;

        if (ossl_time_is_infinite(deadline))
            timeout_ms = -1;
        else
            timeout_ms = ossl_time2ms(ossl_time_subtract(deadline,
                ossl_time_now()));

        rc = poll(rpb->pfd_heap != NULL ? rpb->pfd_heap : rpb->pfds,
            rpb->pfd_num, timeout_ms);
    } while (rc == -1 && get_last_socket_error_is_eintr());
#endif

    return rc < 0 ? 0 : 1;
}
