// Syd: rock-solid application kernel
// src/kernel/net/sendto.rs: sendto(2) handler
//
// Copyright (c) 2023, 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

// SAFETY: This module has been liberated from unsafe code!
#![forbid(unsafe_code)]

use std::os::{fd::AsRawFd, unix::ffi::OsStrExt};

use libseccomp::ScmpNotifResp;
use nix::{
    errno::Errno,
    sys::socket::{send, sendto, AddressFamily, SockaddrLike, SockaddrStorage},
};

use crate::{
    cache::UnixVal,
    compat::{getsockdomain, MsgFlags},
    config::MAX_RW_COUNT,
    fd::{get_nonblock, has_recv_timeout, SafeOwnedFd},
    kernel::net::to_msgflags,
    path::XPath,
    req::UNotifyEventRequest,
};

pub(crate) fn handle_sendto(
    fd: SafeOwnedFd,
    args: &[u64; 6],
    request: &UNotifyEventRequest,
    addr: Option<(SockaddrStorage, SockaddrStorage)>,
    restrict_oob: bool,
) -> Result<ScmpNotifResp, Errno> {
    // Truncate flags to 32-bit keeping unknown flags.
    let flags = to_msgflags(args[3]);

    // Reject MSG_OOB as necessary.
    if restrict_oob && flags.contains(MsgFlags::MSG_OOB) {
        // Signal no support to let the sandbox process handle the error
        // gracefully. This is consistent with the Linux kernel.
        return Err(Errno::EOPNOTSUPP);
    }

    // The length argument to the sendto(2) call must not be fully
    // trusted, it can be overly large, and allocating a Vector of that
    // capacity may overflow. It is valid for the length to be zero to
    // send an empty message. Buffer read from sandbox process MUST be
    // zeroized on drop.
    let len = usize::try_from(args[2])
        .or(Err(Errno::EINVAL))?
        .min(*MAX_RW_COUNT); // Cap count at MAX_RW_COUNT.

    // read_vec_zeroed returns an empty Vec with len==0,
    // without performing any memory reads.
    let buf = request.read_vec_zeroed(args[1], len)?;

    // Record blocking call so it can get invalidated.
    let req = request.scmpreq;
    let is_blocking = if !flags.contains(MsgFlags::MSG_DONTWAIT) && !get_nonblock(&fd)? {
        let ignore_restart = has_recv_timeout(&fd)?;

        // Record the blocking call.
        request.cache.add_sys_block(req, ignore_restart)?;

        true
    } else {
        false
    };

    let result = if let Some((ref addr, _)) = addr {
        // Connection-less socket.
        sendto(fd.as_raw_fd(), &buf, addr, flags.into())
    } else {
        // Connection mode socket, no address specified.
        send(fd.as_raw_fd(), &buf, flags.into())
    };

    // Remove invalidation record unless interrupted.
    if is_blocking {
        request
            .cache
            .del_sys_block(req.id, matches!(result, Err(Errno::EINTR)))?;
    }

    // Send SIGPIPE for EPIPE unless MSG_NOSIGNAL is set.
    #[expect(clippy::cast_possible_wrap)]
    Ok(match result {
        Ok(n) => {
            // Record sender PID for SCM_PIDFD/SO_PASSCRED fixup at recvmsg(2).
            if addr
                .as_ref()
                .map(|(addr, _)| addr.family() == Some(AddressFamily::Unix))
                .unwrap_or_else(|| {
                    getsockdomain(&fd)
                        .map(|domain| domain == libc::AF_UNIX)
                        .unwrap_or(false)
                })
            {
                // Look up destination's device and inode to disambiguate at recv(2).
                let (ddev, dino) = addr
                    .as_ref()
                    .and_then(|(_, argaddr)| argaddr.as_unix_addr())
                    .and_then(|unix| unix.path())
                    .map(|path| XPath::from_bytes(path.as_os_str().as_bytes()))
                    .and_then(|path| request.lookup_unix_vfs_id(path).ok())
                    .map_or((None, None), |(dev, ino)| (Some(dev), Some(ino)));
                let mut val = UnixVal::default();
                if let (Some(dev), Some(ino)) = (ddev, dino) {
                    if val.dest.try_reserve(1).is_ok() {
                        val.dest.push((dev, ino));
                    }
                }
                let _ = request.add_unix(&fd, req.pid(), val);
            }
            request.return_syscall(n as i64)
        }
        Err(Errno::EPIPE) if !flags.contains(MsgFlags::MSG_NOSIGNAL) => {
            request.pidfd_kill(libc::SIGPIPE)?;
            request.fail_syscall(Errno::EPIPE)
        }
        Err(errno) => request.fail_syscall(errno),
    })
}
