/*-*- mode:c;indent-tabs-mode:nil;c-basic-offset:2;tab-width:8;coding:utf-8 -*-│
│vi: set net ft=c ts=2 sts=2 sw=2 fenc=utf-8                                :vi│
╞══════════════════════════════════════════════════════════════════════════════╡
│ Copyright 2020 Justine Alexandra Roberts Tunney                              │
│                                                                              │
│ Permission to use, copy, modify, and/or distribute this software for         │
│ any purpose with or without fee is hereby granted, provided that the         │
│ above copyright notice and this permission notice appear in all copies.      │
│                                                                              │
│ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL                │
│ WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED                │
│ WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE             │
│ AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL         │
│ DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR        │
│ PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER               │
│ TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR             │
│ PERFORMANCE OF THIS SOFTWARE.                                                │
╚─────────────────────────────────────────────────────────────────────────────*/
#include "libc/assert.h"
#include "libc/calls/calls.h"
#include "libc/calls/internal.h"
#include "libc/calls/sig.internal.h"
#include "libc/calls/syscall-sysv.internal.h"
#include "libc/calls/syscall_support-nt.internal.h"
#include "libc/dce.h"
#include "libc/errno.h"
#include "libc/intrin/asan.internal.h"
#include "libc/intrin/describeflags.internal.h"
#include "libc/intrin/safemacros.internal.h"
#include "libc/intrin/strace.internal.h"
#include "libc/macros.internal.h"
#include "libc/nt/enum/filetype.h"
#include "libc/nt/enum/wait.h"
#include "libc/nt/errors.h"
#include "libc/nt/files.h"
#include "libc/nt/struct/byhandlefileinformation.h"
#include "libc/nt/winsock.h"
#include "libc/sock/internal.h"
#include "libc/sock/sendfile.internal.h"
#include "libc/str/str.h"
#include "libc/sysv/errfuns.h"

// sendfile() isn't specified as raising eintr
static textwindows int SendfileBlock(int64_t handle,
                                     struct NtOverlapped *overlapped) {
  int rc;
  uint32_t i, got, flags = 0;
  if (WSAGetLastError() != kNtErrorIoPending &&
      WSAGetLastError() != WSAEINPROGRESS) {
    NTTRACE("TransmitFile failed %lm");
    return __winsockerr();
  }
  for (;;) {
    i = WSAWaitForMultipleEvents(1, &overlapped->hEvent, true,
                                 __SIG_POLLING_INTERVAL_MS, true);
    if (i == kNtWaitFailed) {
      NTTRACE("WSAWaitForMultipleEvents failed %lm");
      return __winsockerr();
    } else if (i == kNtWaitTimeout || i == kNtWaitIoCompletion) {
      if (_check_interrupts(true, g_fds.p)) return -1;
#if _NTTRACE
      POLLTRACE("WSAWaitForMultipleEvents...");
#endif
    } else {
      break;
    }
  }
  if (!WSAGetOverlappedResult(handle, overlapped, &got, false, &flags)) {
    NTTRACE("WSAGetOverlappedResult failed %lm");
    return __winsockerr();
  }
  return got;
}

static dontinline textwindows ssize_t sys_sendfile_nt(
    int outfd, int infd, int64_t *opt_in_out_inoffset, uint32_t uptobytes) {
  ssize_t rc;
  int64_t ih, oh, pos, eof, offset;
  struct NtByHandleFileInformation wst;
  if (!__isfdkind(infd, kFdFile)) return ebadf();
  if (!__isfdkind(outfd, kFdSocket)) return ebadf();
  ih = g_fds.p[infd].handle;
  oh = g_fds.p[outfd].handle;
  if (!SetFilePointerEx(ih, 0, &pos, SEEK_CUR)) {
    return __winerr();
  }
  if (opt_in_out_inoffset) {
    offset = *opt_in_out_inoffset;
  } else {
    offset = pos;
  }
  if (GetFileInformationByHandle(ih, &wst)) {
    // TransmitFile() returns EINVAL if `uptobytes` goes past EOF.
    eof = (uint64_t)wst.nFileSizeHigh << 32 | wst.nFileSizeLow;
    if (offset + uptobytes > eof) {
      uptobytes = eof - offset;
    }
  } else {
    return ebadf();
  }
  struct NtOverlapped ov = {
      .Pointer = (void *)(intptr_t)offset,
      .hEvent = WSACreateEvent(),
  };
  if (TransmitFile(oh, ih, uptobytes, 0, &ov, 0, 0)) {
    rc = uptobytes;
  } else {
    rc = SendfileBlock(oh, &ov);
  }
  if (rc != -1) {
    if (opt_in_out_inoffset) {
      *opt_in_out_inoffset = offset + rc;
      _npassert(SetFilePointerEx(ih, pos, 0, SEEK_SET));
    } else {
      _npassert(SetFilePointerEx(ih, offset + rc, 0, SEEK_SET));
    }
  }
  WSACloseEvent(ov.hEvent);
  return rc;
}

static ssize_t sys_sendfile_bsd(int outfd, int infd,
                                int64_t *opt_in_out_inoffset,
                                size_t uptobytes) {
  ssize_t rc;
  int64_t offset, sbytes;
  if (opt_in_out_inoffset) {
    offset = *opt_in_out_inoffset;
  } else if ((offset = lseek(infd, 0, SEEK_CUR)) == -1) {
    return -1;
  }
  if (IsFreebsd()) {
    rc = sys_sendfile_freebsd(infd, outfd, offset, uptobytes, 0, &sbytes, 0);
    if (rc == -1 && errno == ENOBUFS) errno = ENOMEM;
  } else {
    sbytes = uptobytes;
    rc = sys_sendfile_xnu(infd, outfd, offset, &sbytes, 0, 0);
  }
  if (rc == -1 && errno == ENOTSOCK) errno = EBADF;
  if (rc != -1) {
    if (opt_in_out_inoffset) {
      *opt_in_out_inoffset += sbytes;
    } else {
      _npassert(lseek(infd, offset + sbytes, SEEK_SET) == offset + sbytes);
    }
    return sbytes;
  } else {
    return -1;
  }
}

/**
 * Transfers data from file to network.
 *
 * @param outfd needs to be a socket
 * @param infd needs to be a file
 * @param opt_in_out_inoffset may be specified for pread()-like behavior
 *     in which case the file position won't be changed; otherwise, this
 *     shall read from the file pointer which is advanced accordingly
 * @param uptobytes is the maximum number of bytes to send; some platforms
 *     block until everything's sent, whereas others won't; the behavior of
 *     zero is undefined; this value may overlap the end of file in which
 *     case what remains is sent; this is silently reduced to `0x7ffff000`
 * @return number of bytes transmitted which may be fewer than requested in
 *     which case caller must be prepared to call sendfile() again
 * @raise ESPIPE on Linux RHEL7+ if offset is used but `infd` isn't seekable,
 *     otherwise this could be EINVAL
 * @raise EPIPE on most systems if socket has been shutdown for reading or
 *     the remote end closed the connection, otherwise this could be EINVAL
 * @raise EBADF if `outfd` isn't a valid writeable stream sock descriptor
 * @raise EAGAIN if `O_NONBLOCK` is in play and it would have blocked
 * @raise EBADF if `infd` isn't a valid readable file descriptor
 * @raise EFAULT if `opt_in_out_inoffset` is a bad pointer
 * @raise EINVAL if `*opt_in_out_inoffset` is negative
 * @raise EOVERFLOW is documented as possible on Linux
 * @raise EIO if `infd` had a low-level i/o error
 * @raise ENOMEM if we require more vespene gas
 * @raise ENOTCONN if `outfd` isn't connected
 * @raise ENOSYS on NetBSD and OpenBSD
 * @see copy_file_range() for file ↔ file
 * @see splice() for fd ↔ pipe
 */
ssize_t sendfile(int outfd, int infd, int64_t *opt_in_out_inoffset,
                 size_t uptobytes) {
  ssize_t rc;

  // We must reduce this due to the uint32_t type conversion on Windows
  // which has a maximum of 0x7ffffffe. It also makes sendfile(..., -1)
  // less error prone, since Linux may EINVAL if greater than INT64_MAX
  uptobytes = MIN(uptobytes, 0x7ffff000);

  if (IsAsan() && opt_in_out_inoffset &&
      !__asan_is_valid(opt_in_out_inoffset, 8)) {
    rc = efault();
  } else if (IsLinux()) {
    rc = sys_sendfile(outfd, infd, opt_in_out_inoffset, uptobytes);
  } else if (IsFreebsd() || IsXnu()) {
    rc = sys_sendfile_bsd(outfd, infd, opt_in_out_inoffset, uptobytes);
  } else if (IsWindows()) {
    rc = sys_sendfile_nt(outfd, infd, opt_in_out_inoffset, uptobytes);
  } else {
    rc = enosys();
  }

  STRACE("sendfile(%d, %d, %p, %'zu) → %ld% m", outfd, infd,
         DescribeInOutInt64(rc, opt_in_out_inoffset), uptobytes, rc);
  return rc;
}