#![allow(clippy::undocumented_unsafe_blocks)]

use std::{mem, os::fd::AsRawFd, ptr};

use libloading::os::unix::Symbol;
use nix::{
    errno::Errno,
    libc,
    sys::signal::{kill, sigprocmask, SigSet, SigmaskHow, Signal},
    unistd::{close, getpid, read, write},
};

use crate::{
    caps, safe_drop_cap,
    unshare::{
        error::ErrorCode as Err,
        run::{ChildInfo, Exe},
    },
};

unsafe fn fail_errno(code: Err, errno: i32) -> ! {
    let msg = match code {
        Err::CapSet => b"syd: capset error\0".as_ptr(),
        Err::Exec => b"syd: exec error\0".as_ptr(),
        Err::ParentDeathSignal => b"syd: parent-death-signal error\0".as_ptr(),
        Err::PreExec => b"syd: pre-exec error\0".as_ptr(),
        Err::ProcessStop => b"syd: error stopping process\0".as_ptr(),
        Err::ResetSignal => b"syd: error reseting signals\0".as_ptr(),
        Err::Seccomp => b"syd: seccomp error\0".as_ptr(),
        Err::SeccompSendFd => b"syd: seccomp send notify-fd error\0".as_ptr(),
        Err::SeccompWaitFd => b"syd: seccomp wait for notify-fd error\0".as_ptr(),
        #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
        Err::SetTSC => b"syd: set-tsc error\0".as_ptr(),
    };
    errno::set_errno(errno::Errno(errno));
    libc::perror(msg as *const libc::c_char);
    libc::_exit(errno);
}

macro_rules! fail_safe {
    ($child:expr, $error:expr) => {
        let errno = Errno::last_raw();
        unsafe { fail_errno($error, errno) };
    };
}

macro_rules! fail_errno_safe {
    ($child:expr, $error:expr, $errno:expr) => {
        unsafe { fail_errno($error, $errno) };
    };
}

#[allow(clippy::cognitive_complexity)]
pub extern "C" fn child_after_clone(arg: *mut libc::c_void) -> libc::c_int {
    // SAFETY: arg is a valid ChildInfo structure.
    let child: Box<ChildInfo> = unsafe { Box::from_raw(arg as *mut ChildInfo) };

    // 1. Close the write end of the first pipe.
    // 2. Close the read end of the second pipe.
    // We'll write seccomp notify fd to the second pipe,
    // and read the acknowledgement notification from
    // the first pipe.
    let _ = close(child.seccomp_pipefd.0 .1.as_raw_fd());
    let _ = close(child.seccomp_pipefd.1 .0.as_raw_fd());

    if let Some(&sig) = child.cfg.death_sig.as_ref() {
        if unsafe { libc::prctl(libc::PR_SET_PDEATHSIG, sig as libc::c_ulong, 0, 0, 0) } != 0 {
            fail_safe!(child, Err::ParentDeathSignal);
        }
    }

    if child.cfg.restore_sigmask {
        // Reset blocking signals.
        // Step 1: Reset the signal mask using pthread_sigmask.
        unsafe {
            let mut sigmask: libc::sigset_t = mem::zeroed();
            libc::sigemptyset(&mut sigmask);
            libc::pthread_sigmask(libc::SIG_SETMASK, &sigmask, ptr::null_mut());
        }
        // Step 2: Unblock all signals using sigprocmask.
        let sigmask = SigSet::all();
        if let Err(errno) = sigprocmask(SigmaskHow::SIG_UNBLOCK, Some(&sigmask), None) {
            fail_errno_safe!(child, Err::ResetSignal, errno as i32);
        }

        // Reset all signals to their default dispositions.
        if let Err(errno) = crate::reset_signals() {
            fail_errno_safe!(child, Err::ResetSignal, errno as i32);
        }
    }

    if let Some(callback) = &child.pre_exec {
        if let Err(e) = callback() {
            fail_errno_safe!(child, Err::PreExec, e as i32);
        }
    }

    #[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
    if child.cfg.deny_tsc && unsafe { libc::prctl(libc::PR_SET_TSC, libc::PR_TSC_SIGSEGV) } != 0 {
        fail_safe!(child, Err::SetTSC);
    }

    if child.cfg.stop {
        // Stop the process to give the parent a chance to seize us and set ptrace options.
        // This must happen _before_ loading the seccomp filter.
        if let Err(e) = kill(getpid(), Signal::SIGSTOP) {
            fail_errno_safe!(child, Err::ProcessStop, e as i32);
        }
    }

    if let Some(seccomp_filter) = &child.seccomp_filter {
        // We'll write seccomp notify fd to the second pipe,
        // and read the acknowledgement notification from
        // the first pipe.
        let pipe_ro = &child.seccomp_pipefd.0 .0;
        let pipe_rw = &child.seccomp_pipefd.1 .1;

        // Load the seccomp filter.
        if seccomp_filter.load().is_err() {
            fail_safe!(child, Err::Seccomp);
        }

        // Get seccomp notification fd.
        let fd = match seccomp_filter.get_notify_fd() {
            Ok(fd) => fd,
            Err(_) => {
                fail_safe!(child, Err::Seccomp);
            }
        };

        // Write the value of the fd to the pipe.
        // Handle partial writes and interrupts.
        let fd_bytes = fd.to_le_bytes();
        let mut nwrite = 0;
        while nwrite < fd_bytes.len() {
            #[allow(clippy::arithmetic_side_effects)]
            match write(pipe_rw, &fd_bytes[nwrite..]) {
                Ok(0) => {
                    // Parent died before reading,
                    // this should never happen.
                    let _ = close(fd);
                    fail_errno_safe!(child, Err::SeccompSendFd, Errno::EIO as i32);
                }
                Ok(n) => nwrite += n,
                Err(Errno::EINTR | Errno::EAGAIN) => continue,
                Err(errno) => {
                    let _ = close(fd);
                    fail_errno_safe!(child, Err::SeccompSendFd, errno as i32);
                }
            }
        }
        let _ = close(pipe_rw.as_raw_fd()); // No longer necessary.

        // Wait for the parent to get the file descriptor.
        // Read success notification from the pipe.
        // Handle interrupts, partial read is impossible.
        let mut ack = [0u8];
        loop {
            match read(pipe_ro.as_raw_fd(), &mut ack) {
                Ok(0) => {
                    // Parent died before writing,
                    // this should never happen.
                    let _ = close(pipe_ro.as_raw_fd());
                    let _ = close(fd);
                    fail_errno_safe!(child, Err::SeccompWaitFd, Errno::EIO as i32);
                }
                Ok(_) => break,
                Err(Errno::EINTR | Errno::EAGAIN) => continue,
                Err(errno) => {
                    let _ = close(pipe_ro.as_raw_fd());
                    let _ = close(fd);
                    fail_errno_safe!(child, Err::SeccompWaitFd, errno as i32);
                }
            }
        }

        // Close the file descriptors and go on.
        let _ = close(pipe_ro.as_raw_fd());
        let _ = close(fd);
    }

    // Drop CAP_SYS_PTRACE late as syd may need it.
    if !child.cfg.keep && safe_drop_cap(caps::Capability::CAP_SYS_PTRACE).is_err() {
        fail_safe!(child, Err::CapSet);
    }
    if caps::securebits::set_keepcaps(true).is_err() {
        fail_safe!(child, Err::CapSet);
    }

    match child.exe {
        Exe::Library(lib) => unsafe {
            let fun: Symbol<unsafe extern "C" fn() -> i32> = match lib.get(b"syd_main") {
                Ok(fun) => fun,
                Err(_) => nix::libc::_exit(nix::libc::EINVAL),
            };
            nix::libc::_exit(fun());
        },
        Exe::Program((filename, ref args)) => {
            let args = &args[..];
            unsafe { libc::execvp(filename, args.as_ptr()) };
            fail_safe!(child, Err::Exec);
        }
    }
}
