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

use std::{mem::size_of, os::fd::RawFd};

use libseccomp::ScmpNotifResp;
use nix::{
    errno::Errno,
    sys::socket::{getsockopt, sockopt::PeerPidfd},
    unistd::Pid,
};

use crate::{
    confine::{is_valid_ptr, scmp_arch_bits},
    fd::{peer_creds, SafeOwnedFd},
    req::UNotifyEventRequest,
};

pub(crate) fn handle_getsockopt(
    fd: SafeOwnedFd,
    request: &UNotifyEventRequest,
    args: &[u64; 6],
    randomize_fds: bool,
) -> Result<ScmpNotifResp, Errno> {
    const SO_PEERCRED: u64 = libc::SO_PEERCRED as u64;
    const SO_PEERPIDFD: u64 = libc::SO_PEERPIDFD as u64;

    if args[1] != libc::SOL_SOCKET as u64 {
        // We only hook into SOL_SOCKET, however socketcall(2) can still end up here.
        // SAFETY: No pointer dereference in access check.
        return Ok(unsafe { request.continue_syscall() });
    }

    match args[2] {
        SO_PEERCRED => handle_getsockopt_peercred(fd, request, args),
        SO_PEERPIDFD => handle_getsockopt_peerpidfd(fd, request, args, randomize_fds),
        _ => {
            // SAFETY: No pointer dereference in access check.
            Ok(unsafe { request.continue_syscall() })
        }
    }
}

fn handle_getsockopt_peercred(
    fd: SafeOwnedFd,
    request: &UNotifyEventRequest,
    args: &[u64; 6],
) -> Result<ScmpNotifResp, Errno> {
    // optval and optlen pointers in tracee
    let optval_ptr = args[3];
    let optlen_ptr = args[4];

    // optlen pointer must not be NULL.
    if !is_valid_ptr(optlen_ptr, request.scmpreq.data.arch) {
        return Err(Errno::EFAULT);
    }

    // Check for 32-bit tracee.
    let req = request.scmpreq;
    let is32 = scmp_arch_bits(req.data.arch) == 32;

    // socklen_t is a 32-bit integer on both 32-bit and 64-bit.
    const SIZEOF_SOCKLEN_T: usize = size_of::<libc::socklen_t>();

    // Read *optlen from tracee.
    let mut len_buf = [0u8; SIZEOF_SOCKLEN_T];
    let read = request.read_mem(&mut len_buf, optlen_ptr, SIZEOF_SOCKLEN_T)?;
    if read != SIZEOF_SOCKLEN_T {
        return Err(Errno::EINVAL);
    }

    // Convert bytes to usize respecting native endianness.
    let orig_optlen = u32::from_ne_bytes([len_buf[0], len_buf[1], len_buf[2], len_buf[3]]) as usize;

    // If optval == NULL but *optlen > 0, kernel returns EFAULT.
    if !is_valid_ptr(optval_ptr, request.scmpreq.data.arch) && orig_optlen > 0 {
        return Err(Errno::EFAULT);
    }

    // Build credentials to return.
    let ucred = peer_creds(&fd)?;
    let uid = ucred.uid();
    let gid = ucred.gid();
    let pid = if ucred.pid() != Pid::this().as_raw() {
        ucred.pid()
    } else {
        request.fix_cred_pid(&fd).as_raw()
    };

    // Prepare ucred size for tracee ABI.
    let ucred_size = if is32 {
        12usize
    } else {
        size_of::<libc::ucred>()
    };

    // How many bytes we'll actually copy back.
    let to_copy = std::cmp::min(orig_optlen, ucred_size);

    if to_copy > 0 {
        // For 32-bit tracee: 3 x 32-bit little/big-endian values (pid, uid, gid)
        #[expect(clippy::cast_sign_loss)]
        if is32 {
            let mut b = [0u8; 12];

            b[0..4].copy_from_slice(&(pid as u32).to_ne_bytes());
            b[4..8].copy_from_slice(&uid.to_ne_bytes());
            b[8..12].copy_from_slice(&gid.to_ne_bytes());

            // Write only the first to_copy bytes.
            request.write_mem(&b[..to_copy], optval_ptr)?;
        } else {
            // Native layout: use libc::ucred.
            let native = libc::ucred { pid, uid, gid };

            let native_bytes: &[u8] = unsafe {
                // SAFETY: Read native bytes as byte slice.
                std::slice::from_raw_parts(
                    (&raw const native) as *const u8,
                    size_of::<libc::ucred>(),
                )
            };
            request.write_mem(&native_bytes[..to_copy], optval_ptr)?;
        }
    }

    // Write back the resulting length into *optlen.
    #[expect(clippy::cast_possible_truncation)]
    let v = (to_copy as u32).to_ne_bytes();
    request.write_mem(&v, optlen_ptr)?;

    Ok(request.return_syscall(0))
}

fn handle_getsockopt_peerpidfd(
    fd: SafeOwnedFd,
    request: &UNotifyEventRequest,
    args: &[u64; 6],
    randomize_fds: bool,
) -> Result<ScmpNotifResp, Errno> {
    // optval and optlen pointers in tracee
    let optval_ptr = args[3];
    let optlen_ptr = args[4];

    // optlen pointer must not be NULL.
    if !is_valid_ptr(optlen_ptr, request.scmpreq.data.arch) {
        return Err(Errno::EFAULT);
    }

    // socklen_t is a 32-bit integer on both 32-bit and 64-bit.
    const SIZEOF_SOCKLEN_T: usize = size_of::<libc::socklen_t>();

    // Read *optlen from tracee.
    let mut len_buf = [0u8; SIZEOF_SOCKLEN_T];
    let read = request.read_mem(&mut len_buf, optlen_ptr, SIZEOF_SOCKLEN_T)?;
    if read != SIZEOF_SOCKLEN_T {
        return Err(Errno::EINVAL);
    }

    // Convert bytes to usize respecting native endianness.
    let orig_optlen = u32::from_ne_bytes([len_buf[0], len_buf[1], len_buf[2], len_buf[3]]) as usize;

    // If optval == NULL but *optlen > 0, kernel returns EFAULT.
    if !is_valid_ptr(optval_ptr, request.scmpreq.data.arch) && orig_optlen > 0 {
        return Err(Errno::EFAULT);
    }

    // SO_PEERPIDFD returns a single RawFd.
    const SIZEOF_FD: usize = size_of::<RawFd>();

    // Callers must provide at least sizeof(RawFd) buffer.
    if orig_optlen < SIZEOF_FD {
        return Err(Errno::EINVAL);
    }

    // Fix PIDFd as necessary.
    let ucred = peer_creds(&fd)?;
    let pidfd = if ucred.pid() != Pid::this().as_raw() {
        getsockopt(&fd, PeerPidfd)?.into()
    } else {
        request.fix_scm_pidfd(&fd)?
    };

    // Add the fd to sandbox process, close our copy.
    let pidfd = request.add_fd(pidfd, true /* close-on-exec*/, randomize_fds)?;

    // Write the fd value into the tracee's optval buffer.
    let pidfd = pidfd.to_ne_bytes();
    request.write_mem(&pidfd, optval_ptr)?;

    // Write back sizeof(RawFd) into *optlen.
    #[expect(clippy::cast_possible_truncation)]
    let v = (SIZEOF_FD as u32).to_ne_bytes();
    request.write_mem(&v, optlen_ptr)?;

    Ok(request.return_syscall(0))
}
