//
// Syd: rock-solid application kernel
// src/test/util.rs: Utilities for integration tests
//
// Copyright (c) 2023, 2024, 2025, 2026 Ali Polatel <alip@chesswob.org>
//
// SPDX-License-Identifier: GPL-3.0

#![allow(clippy::disallowed_methods)]
#![allow(clippy::disallowed_types)]

use std::{
    env,
    error::Error,
    ffi::OsStr,
    fmt,
    fs::{canonicalize, File},
    io::Write,
    net::{Ipv6Addr, SocketAddrV6, TcpListener},
    os::fd::RawFd,
    path::{Path, PathBuf},
    process::{Child, Command, ExitStatus, Output, Stdio},
    sync::LazyLock,
    time::Duration,
};

use memchr::memmem;
use nix::{
    errno::Errno,
    sys::{
        resource::{Resource, RLIM_INFINITY},
        utsname::uname,
    },
    unistd::isatty,
};
use syd::{config::*, confine::confine_rlimit};

#[derive(Debug)]
pub struct TestError(pub String);
pub type TestResult = Result<(), TestError>;

impl fmt::Display for TestError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.0)
    }
}

impl<E: Error> From<E> for TestError {
    fn from(err: E) -> Self {
        TestError(err.to_string())
    }
}

#[macro_export]
macro_rules! say {
    ($($arg:tt)*) => {{
        let msg = format!($($arg)*);
        let pre = "\r\x1b[K";

        if *$crate::util::SYD_TEST_TTY {
            eprint!("{pre}{msg}");
        } else {
            eprintln!("{msg}");
        }
    }};
}

#[macro_export]
macro_rules! assert {
    ($cond:expr) => {
        if !$cond {
            return Err(TestError(format!("Assertion failed: {}", stringify!($cond))));
        }
    };
    ($cond:expr, $($arg:tt)*) => {
        if !$cond {
            return Err(TestError(format!("Assertion failed: {}: {}", stringify!($cond), format_args!($($arg)*))));
        }
    };
}

#[macro_export]
macro_rules! assert_eq {
    ($left:expr, $right:expr) => {
        if $left != $right {
            return Err(TestError(format!("Assertion failed in {}:{}: (left: `{}`, right: `{}`)", file!(), line!(), $left, $right)));
        }
    };
    ($left:expr, $right:expr, $($arg:tt)*) => {
        if $left != $right {
            return Err(TestError(format!("Assertion failed in {}:{}: (left: `{}`, right: `{}`): {}", file!(), line!(), $left, $right, format_args!($($arg)*))));
        }
    };
}

#[macro_export]
macro_rules! assert_ne {
    ($left:expr, $right:expr) => {
        if $left == $right {
            return Err(TestError(format!("Assertion failed in {}:{}: (left: `{}`, right: `{}`)", file!(), line!(), $left, $right)));
        }
    };
    ($left:expr, $right:expr, $($arg:tt)*) => {
        if $left == $right {
            return Err(TestError(format!("Assertion failed in {}:{}: (left: `{}`, right: `{}`): {}", file!(), line!(), $left, $right, format_args!($($arg)*))));
        }
    };
}

#[macro_export]
macro_rules! fixup {
    ($cond:expr) => {
        if $cond {
            return Err(TestError(format!("Known issue fixed in {}:{}", file!(), line!())));
        } else {
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            eprintln!("Warning: Known issue still present in {}:{}", file!(), line!());
        }
    };
    ($cond:expr, $($arg:tt)*) => {
        if $cond {
            return Err(TestError(format!("Known issue fixed in {}:{}: {}", file!(), line!(), format_args!($($arg)*))));
        } else {
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            eprintln!("Warning: Known issue still present in {}:{}: {}", file!(), line!(), format_args!($($arg)*));
        }
    };
}

#[macro_export]
macro_rules! ignore {
    ($cond:expr) => {
        if $cond {
            eprintln!("Warning: Known issue fixed in {}:{}", file!(), line!());
        } else {
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            eprintln!("Warning: Known issue still present in {}:{}", file!(), line!());
        }
    };
    ($cond:expr, $($arg:tt)*) => {
        if $cond {
            eprintln!("Warning: Known issue fixed in {}:{}", file!(), line!());
        } else {
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            eprintln!("Warning: Known issue still present in {}:{}: {}", file!(), line!(), format_args!($($arg)*));
        }
    };
}

#[macro_export]
macro_rules! assert_status_aborted {
    ($status:expr) => {{
        const XABRT: i32 = 128 + libc::SIGABRT;
        const XBUS: i32 = 128 + libc::SIGBUS;
        const XILL: i32 = 128 + libc::SIGILL;
        const XSEGV: i32 = 128 + libc::SIGSEGV;
        assert_status_code_matches!($status, XABRT | XBUS | XILL | XSEGV);
    }};
}

#[macro_export]
macro_rules! assert_status_panicked {
    ($status:expr) => {{
        // If the main thread panics it will terminate all your threads
        // and end your program with code 101.
        // See: https://doc.rust-lang.org/std/macro.panic.html
        assert_status_code!($status, 101);
    }};
}

#[macro_export]
macro_rules! assert_status_timeout_exceeded {
    ($status:expr) => {{
        assert_status_code!($status, 124);
    }};
}

#[macro_export]
macro_rules! assert_status_code_matches {
    ($status:expr, $($pattern:tt)+) => {{
        let code = $status.code().unwrap_or(127);
        $crate::assert!(
            matches!(code, $($pattern)+),
            "code: {code} status: {:?}",
            $status
        );
    }};
}

#[macro_export]
macro_rules! assert_status_code {
    ($status:expr, $expected:expr) => {
        let code = $status.code().unwrap_or(127);
        $crate::assert_eq!(code, $expected, "code:{code} status:{:?}", $status);
    };
}

#[macro_export]
macro_rules! assert_status_ok {
    ($status:expr) => {
        let code = $status.code().unwrap_or(127);
        $crate::assert!($status.success(), "code:{code} status:{:?}", $status);
    };
}

#[macro_export]
macro_rules! assert_status_not_ok {
    ($status:expr) => {
        let code = $status.code().unwrap_or(127);
        $crate::assert!(!$status.success(), "code:{code} status:{:?}", $status);
    };
}

#[macro_export]
macro_rules! assert_status_bad_file {
    ($status:expr) => {
        $crate::assert_status_code!($status, libc::EBADF);
    };
}

#[macro_export]
macro_rules! assert_status_bad_message {
    ($status:expr) => {
        $crate::assert_status_code!($status, libc::EBADMSG);
    };
}

#[macro_export]
macro_rules! assert_status_access_denied {
    ($status:expr) => {
        $crate::assert_status_code!($status, libc::EACCES);
    };
}

#[macro_export]
macro_rules! assert_status_permission_denied {
    ($status:expr) => {
        $crate::assert_status_code!($status, libc::EPERM);
    };
}

#[macro_export]
macro_rules! assert_status_no_such_process {
    ($status:expr) => {
        $crate::assert_status_code!($status, libc::ESRCH);
    };
}

#[macro_export]
macro_rules! assert_status_broken_pipe {
    ($status:expr) => {
        $crate::assert_status_code!($status, libc::EPIPE);
    };
}

#[macro_export]
macro_rules! assert_status_busy {
    ($status:expr) => {
        $crate::assert_status_code!($status, libc::EBUSY);
    };
}

#[macro_export]
macro_rules! assert_status_connection_refused {
    ($status:expr) => {
        $crate::assert_status_code!($status, libc::ECONNREFUSED);
    };
}

#[macro_export]
macro_rules! assert_status_faulted {
    ($status:expr) => {
        $crate::assert_status_code!($status, libc::EFAULT);
    };
}

#[macro_export]
macro_rules! assert_status_hidden {
    ($status:expr) => {
        $crate::assert_status_code!($status, libc::ENOENT);
    };
}

#[macro_export]
macro_rules! assert_status_notdir {
    ($status:expr) => {
        $crate::assert_status_code!($status, libc::ENOTDIR);
    };
}

#[macro_export]
macro_rules! assert_status_notty {
    ($status:expr) => {
        $crate::assert_status_code!($status, libc::ENOTTY);
    };
}

#[macro_export]
macro_rules! assert_status_illegal_sequence {
    ($status:expr) => {
        $crate::assert_status_code!($status, libc::EILSEQ);
    };
}

#[macro_export]
macro_rules! assert_status_invalid {
    ($status:expr) => {
        $crate::assert_status_code!($status, libc::EINVAL);
    };
}

#[macro_export]
macro_rules! assert_status_interrupted {
    ($status:expr) => {
        $crate::assert_status_code!($status, libc::EINTR);
    };
}

#[macro_export]
macro_rules! assert_status_loop {
    ($status:expr) => {
        $crate::assert_status_code!($status, libc::ELOOP);
    };
}

#[macro_export]
macro_rules! assert_status_unimplemented {
    ($status:expr) => {
        $crate::assert_status_code!($status, libc::ENOSYS);
    };
}

#[macro_export]
macro_rules! assert_status_killed {
    ($status:expr) => {
        let code = $status.code().unwrap_or(127);
        $crate::assert_eq!(
            code,
            128 + libc::SIGKILL,
            "code:{code} status:{:?}",
            $status
        );
    };
}

#[macro_export]
macro_rules! assert_status_not_killed {
    ($status:expr) => {
        let code = $status.code().unwrap_or(127);
        $crate::assert_ne!(
            code,
            128 + libc::SIGKILL,
            "code:{code} status:{:?}",
            $status
        );
    };
}

#[macro_export]
macro_rules! assert_status_sigsys {
    ($status:expr) => {
        let code = $status.code().unwrap_or(127);
        $crate::assert_eq!(code, 128 + libc::SIGSYS, "code:{code} status:{:?}", $status);
    };
}

#[macro_export]
macro_rules! assert_status_signaled {
    ($status:expr, $signal:expr) => {
        let signal = $status.signal().unwrap_or(127);
        $crate::assert_eq!(signal, $signal, "signal:{signal} status:{:?}", $status);
    };
}

#[macro_export]
macro_rules! assert_status_not_supported {
    ($status:expr) => {
        let code = $status.code().unwrap_or(127);
        $crate::assert!(
            matches!(code, libc::EAFNOSUPPORT | nix::libc::ENOTSUP),
            "code:{code} status:{:?}",
            $status
        );
    };
}

#[macro_export]
macro_rules! assert_status_operation_not_supported {
    ($status:expr) => {
        let code = $status.code().unwrap_or(127);
        $crate::assert!(
            matches!(code, libc::EOPNOTSUPP),
            "code:{code} status:{:?}",
            $status
        );
    };
}

#[macro_export]
macro_rules! skip_unless_gnu {
    ($($program:expr),* $(,)?) => {{
        let mut all_gnu = true;
        $(
            if !is_program_gnu($program) {
                eprintln!("Test requires {} to be GNU, skipping!", $program);
                std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
                all_gnu = false;
            }
        )*

        if !all_gnu {
            return Ok(());
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_available {
    ($($program:expr),* $(,)?) => {{
        let mut all_available = true;
        $(
            if !is_program_available($program) {
                eprintln!("Test requires {}, skipping!", $program);
                std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
                all_available = false;
            }
        )*

        if !all_available {
            return Ok(());
        }
    }};
}

#[macro_export]
macro_rules! skip_if_32bin {
    () => {{
        if cfg!(target_pointer_width = "32") {
            eprintln!("32-bit target detected, skipping!");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }
    }};
}

#[macro_export]
macro_rules! skip_if_32bin_64host {
    () => {{
        if check_32bin_64host() {
            eprintln!("Binary/Host mismatch, cannot run test, skipping!");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }
    }};
}

#[macro_export]
macro_rules! skip_if_mips {
    () => {{
        if cfg!(any(
            target_arch = "mips",
            target_arch = "mips32r6",
            target_arch = "mips64",
            target_arch = "mips64r6",
        )) {
            eprintln!("Test does not work on MIPS, skipping!");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }
    }};
}

#[macro_export]
macro_rules! skip_if_root {
    () => {{
        if Uid::effective().is_root() {
            eprintln!("Test requires to run as non-root user, skipping!");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        } else {
            eprintln!("Thanks for not running this test as root!");
        }
    }};
}

#[macro_export]
macro_rules! skip_if_fs {
    ($($fs:expr),* $(,)?) => {{
        let cwd = open(".", OFlag::O_PATH | OFlag::O_DIRECTORY, Mode::empty()).unwrap();
        let current_fs = syd::compat::FsType::get(cwd).unwrap();
        $(
            if let Ok(fs_types) = syd::compat::FsType::from_name($fs) {
                if fs_types.contains(&current_fs) {
                    eprintln!("Test does not run correctly on {}, skipping!", $fs);
                    std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
                    return Ok(());
                }
            }
        )*
        eprintln!("Filesystem check passed, continuing...");
    }};
}

#[macro_export]
macro_rules! skip_unless_iproute2 {
    () => {{
        if !check_iproute2() {
            eprintln!("ip is not iproute2, cannot run test, skipping!");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_linux {
    ($need_major:expr, $need_minor:expr) => {{
        let (maj, min) = *syd::config::KERNEL_VERSION;
        let too_old = maj < $need_major || (maj == $need_major && min < $need_minor);
        if too_old {
            eprintln!(
                "Test requires Linux kernel >= {}.{} (current: {maj}.{min}), skipping!",
                $need_major, $need_minor,
            );
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        } else {
            eprintln!(
                "Linux kernel {maj}.{min} satisfies >= {}.{}, proceeding!",
                $need_major, $need_minor,
            );
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_stdin_is_a_tty {
    () => {{
        if !nix::unistd::isatty(std::io::stdin()).unwrap_or(false) {
            eprintln!("Test requires STDIN to be a TTY, skipping!");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        } else {
            eprintln!("STDIN is a TTY, proceeding with test...");
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_stdout_is_a_tty {
    () => {{
        if !nix::unistd::isatty(std::io::stdout()).unwrap_or(false) {
            eprintln!("Test requires STDOUT to be a TTY, skipping!");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        } else {
            eprintln!("STDOUT is a TTY, proceeding with test...");
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_kernel_crypto_is_supported {
    () => {{
        let key = syd::hash::Key::random().unwrap();
        let key_id = match syd::hash::add_key(
            "user",
            "SYD-3-CRYPT-TEST",
            key.as_ref(),
            syd::hash::KEY_SPEC_USER_KEYRING,
        ) {
            Ok(key_id) => key_id,
            Err(nix::errno::Errno::EAFNOSUPPORT | nix::errno::Errno::ENOSYS) => {
                eprintln!("Test requires Linux keyrings(7) API, skipping!");
                std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
                return Ok(());
            }
            Err(nix::errno::Errno::EPERM) => {
                eprintln!("Are you in a container without keyrings access?");
                eprintln!("Test requires Linux keyrings(7) API, skipping!");
                std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
                return Ok(());
            }
            Err(nix::errno::Errno::EACCES) => {
                eprintln!("Is your session keyring attached to your user keyring?");
                eprintln!("Test requires Linux keyrings(7) API, skipping!");
                std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
                return Ok(());
            }
            Err(errno) => {
                return Err(TestError(format!(
                    "Failed to test for Linux keyrings(7) API: {errno}"
                )));
            }
        };
        match syd::hash::aes_ctr_setup(key_id) {
            Ok(fd) => drop(fd),
            Err(nix::errno::Errno::EAFNOSUPPORT | Errno::ENOPROTOOPT) => {
                eprintln!("Test requires Linux Kernel Cryptography API, skipping!");
                std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
                return Ok(());
            }
            Err(nix::errno::Errno::EACCES) => {
                eprintln!("Is your session keyring attached to your user keyring?");
                eprintln!("Test requires Linux keyrings(7) API, skipping!");
                std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
                return Ok(());
            }
            Err(errno) => {
                return Err(TestError(format!(
                    "Failed to test for Linux Kernel Cryptography API: {errno}"
                )));
            }
        }
        match syd::hash::hmac_sha256_setup(key_id) {
            Ok(fd) => drop(fd),
            Err(nix::errno::Errno::EAFNOSUPPORT | Errno::ENOPROTOOPT) => {
                eprintln!("Test requires Linux Kernel Cryptography API, skipping!");
                std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
                return Ok(());
            }
            Err(nix::errno::Errno::EACCES) => {
                eprintln!("Is your session keyring attached to your user keyring?");
                eprintln!("Test requires Linux keyrings(7) API, skipping!");
                std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
                return Ok(());
            }
            Err(errno) => {
                return Err(TestError(format!(
                    "Failed to test for Linux Kernel Cryptography API: {errno}"
                )));
            }
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_xattrs_are_supported {
    () => {{
        let fd = match nix::fcntl::open(
            "xattr.test",
            nix::fcntl::OFlag::O_WRONLY | nix::fcntl::OFlag::O_CREAT | nix::fcntl::OFlag::O_EXCL,
            nix::sys::stat::Mode::from_bits_truncate(0o600),
        ) {
            Ok(fd) => fd,
            Err(errno) => return Err(TestError(format!("Failed to open xattr.test: {errno}"))),
        };

        match syd::xattr::fsetxattr(&fd, "user.syd.crypt.api", b"3", libc::XATTR_CREATE) {
            Ok(_) => {
                let _ = nix::unistd::close(fd);
            }
            Err(nix::errno::Errno::EOPNOTSUPP) => {
                let _ = nix::unistd::close(fd);
                eprintln!("Test requires extended attributes support, skipping!");
                std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
                return Ok(());
            }
            Err(errno) => {
                let _ = nix::unistd::close(fd);
                return Err(TestError(format!(
                    "Failed to test for extended attributes support: {errno}"
                )));
            }
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_unix_diag_is_supported {
    () => {{
        match syd::confine::check_unix_diag() {
            Ok(true) => {
                eprintln!("Unix socket diagnostics are supported, proceeding with test...");
            }
            Ok(false) => {
                eprintln!("Test requires UNIX socket diagnostics support, skipping!");
                std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
                return Ok(());
            }
            Err(errno) => {
                return Err(TestError(format!(
                    "Failed to test for UNIX socket diagnostics: {errno}"
                )));
            }
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_unshare {
    ( $( $ns:expr ),* $(,)? ) => {{
        use nix::sched::CloneFlags;

        // Build combined CloneFlags from the provided string literals.
        let mut flags = CloneFlags::empty();
        $(
            flags |= match $ns {
                "mount"   => CloneFlags::CLONE_NEWNS,
                "uts"     => CloneFlags::CLONE_NEWUTS,
                "ipc"     => CloneFlags::CLONE_NEWIPC,
                "user"    => CloneFlags::CLONE_NEWUSER,
                "pid"     => CloneFlags::CLONE_NEWPID,
                "net"     => CloneFlags::CLONE_NEWNET,
                "cgroup"  => CloneFlags::CLONE_NEWCGROUP,
                "time"    => syd::confine::CLONE_NEWTIME,
                "all"     => CloneFlags::CLONE_NEWNS |  CloneFlags::CLONE_NEWUTS | CloneFlags::CLONE_NEWIPC | CloneFlags::CLONE_NEWUSER | CloneFlags::CLONE_NEWPID | CloneFlags::CLONE_NEWNET | CloneFlags::CLONE_NEWCGROUP | syd::confine::CLONE_NEWTIME,
                other => panic!("Unknown namespace: {other}"),
            };
        )*

        if flags.is_empty() {
            panic!("No namespace given!");
        }

        // Gitlab builds run under docker, and do not permit subnamespaces.
        if *GL_BUILD {
            eprintln!("Test requires Linux namespaces ({:?}), skipping!", [$($ns),*]);
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }

        // Apparmor does not allow use of unprivileged namespaces by default.
        if syd::confine::apparmor_enabled().unwrap_or(false) {
            eprintln!("Apparmor detected!");
            eprintln!("Test requires Linux namespaces ({:?}), skipping!", [$($ns),*]);
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }

        match syd::confine::ns_enabled(flags) {
            Ok(false) => {
                eprintln!("Test requires Linux namespaces ({:?}), skipping!", [$($ns),*]);
                std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
                return Ok(());
            }
            Err(errno) => {
                return Err(TestError(
                    format!("Failed to test for Linux namespaces: {errno}"),
                ));
            }
            _ => {}
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_coredumps {
    () => {{
        if let Err(errno) = enable_coredumps() {
            eprintln!("Failed to enable coredumps: {errno}!");
            eprintln!("Skipping test!");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_exists {
    ($path:expr) => {{
        if !std::path::Path::new($path).exists() {
            eprintln!("Test requires the path \"{}\" to exist, skipping!", $path);
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_bitness {
    ($bitness:expr) => {{
        if !cfg!(target_pointer_width = $bitness) {
            eprintln!("Test requires a {}-bit syd, skipping!", $bitness);
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_feature {
    ($feature:expr) => {{
        if !cfg!(feature = $feature) {
            eprintln!(
                "Test requires syd built with {} feature, skipping!",
                $feature
            );
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_cap {
    ($cap:expr) => {{
        use std::str::FromStr;
        let cap = syd::caps::Capability::from_str(&syd::caps::to_canonical($cap)).expect("cap2str");
        if !syd::caps::has_cap(None, syd::caps::CapSet::Permitted, cap).expect("syd::caps::has_cap")
        {
            eprintln!("Test requires {cap} capability, skipping!");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        } else {
            eprintln!("Capability {cap} set, proceeding with test.");
        }
    }};
}

#[macro_export]
macro_rules! skip_if_landlock_abi_supported {
    ($abi:expr) => {{
        if syd::landlock::ABI::new_current() >= syd::landlock::ABI::from($abi) {
            eprintln!(
                "Test is for Landlock ABI versions older than {}, skipping!",
                $abi
            );
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        } else {
            eprintln!(
                "Landlock ABI {} is NOT supported, proceeding with test...",
                $abi
            );
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_landlock_abi_supported {
    ($abi:expr) => {{
        if syd::landlock::ABI::new_current() < syd::landlock::ABI::from($abi) {
            eprintln!("Test requires Landlock ABI version {}, skipping!", $abi);
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        } else {
            eprintln!(
                "Landlock ABI {} is supported, proceeding with test...",
                $abi
            );
        }
    }};
}

#[macro_export]
macro_rules! skip_if_cross_memory_attach_is_not_enabled {
    () => {
        if !*syd::config::HAVE_CROSS_MEMORY_ATTACH {
            eprintln!("skipping: kernel not configured with CONFIG_CROSS_MEMORY_ATTACH");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }
    };
}

#[macro_export]
macro_rules! skip_if_strace {
    () => {{
        if std::env::var("SYD_TEST_STRACE").is_ok() && std::env::var("SYD_TEST_FORCE").is_err() {
            eprintln!("Test does not work correctly under strace, skipping!");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_strace_can_inject {
    () => {{
        if !check_strace_inject() {
            eprintln!("strace can't inject syscalls, cannot run test, skipping!");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_pty {
    () => {{
        use std::process::Command;

        skip_unless_available!("python");

        // Run python to test for support.
        //
        // TODO: Make syd-tty do this.
        let status = Command::new("python")
            .arg("-c")
            .arg("import os; os.openpty()")
            .status()
            .expect("execute python");

        if status.code().unwrap_or(127) != 0 {
            eprintln!("Test requires PTY access, skipping!");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        } else {
            eprintln!("PTY access is available, proceeding with test...");
        }
    }};
}

#[macro_export]
macro_rules! skip_unless_at_execve_check_is_supported {
    () => {{
        if !*HAVE_AT_EXECVE_CHECK {
            eprintln!("execveat(2) flag AT_EXECVE_CHECK is not supported, skipping!");
            std::env::set_var("SYD_TEST_SOFT_FAIL", "1");
            return Ok(());
        }
    }};
}

/// Holds a `String` to run `syd`.
/// Honours CARGO_BIN_EXE_syd environment variable.
pub static SYD: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd").unwrap_or("syd".to_string()));

/// Holds a `String` to run `syd-aux`.
/// Honours CARGO_BIN_EXE_syd-aux environment variable.
pub static SYD_AUX: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-aux").unwrap_or("syd-aux".to_string()));

/// Holds a `String` to run `syd-bit`.
/// Honours CARGO_BIN_EXE_syd-aux environment variable.
pub static SYD_BIT: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-bit").unwrap_or("syd-bit".to_string()));

/// Holds a `String` to run `syd-cap`.
/// Honours CARGO_BIN_EXE_syd-cap environment variable.
pub static SYD_CAP: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-cap").unwrap_or("syd-cap".to_string()));

/// Holds a `String` to run `syd-dns`.
/// Honours CARGO_BIN_EXE_syd-dns environment variable.
pub static SYD_DNS: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-dns").unwrap_or("syd-dns".to_string()));

/// Holds a `String` to run `syd-env`.
/// Honours CARGO_BIN_EXE_syd-env environment variable.
#[allow(dead_code)]
pub static SYD_ENV: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-env").unwrap_or("syd-env".to_string()));

/// Holds a `String` to run `syd-exec`.
/// Honours CARGO_BIN_EXE_syd-exec environment variable.
pub static SYD_EXEC: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-exec").unwrap_or("syd-exec".to_string()));

/// Holds a `String` to run `syd-aes`.
/// Honours CARGO_BIN_EXE_syd-aes environment variable.
pub static SYD_AES: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-aes").unwrap_or("syd-aes".to_string()));

/// Holds a `String` to run `syd-key`.
/// Honours CARGO_BIN_EXE_syd-key environment variable.
pub static _SYD_KEY: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-key").unwrap_or("syd-key".to_string()));

/// Holds a `String` to run `syd-elf`.
/// Honours CARGO_BIN_EXE_syd-elf environment variable.
pub static SYD_ELF: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-elf").unwrap_or("syd-elf".to_string()));

/// Holds a `String` to run `syd-cpu`.
/// Honours CARGO_BIN_EXE_syd-cpu environment variable.
pub static SYD_CPU: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-cpu").unwrap_or("syd-cpu".to_string()));

/// Holds a `String` to run `syd-hex`.
/// Honours CARGO_BIN_EXE_syd-hex environment variable.
pub static SYD_HEX: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-hex").unwrap_or("syd-hex".to_string()));

/// Holds a `String` to run `syd-info`.
/// Honours CARGO_BIN_EXE_syd-info environment variable.
pub static SYD_INFO: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-info").unwrap_or("syd-info".to_string()));

/// Holds a `String` to run `syd-lock`.
/// Honours CARGO_BIN_EXE_syd-lock environment variable.
pub static SYD_LOCK: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-lock").unwrap_or("syd-lock".to_string()));

/// Holds a `String` to run `syd-mdwe`.
/// Honours CARGO_BIN_EXE_syd-mdwe environment variable.
pub static SYD_MDWE: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-mdwe").unwrap_or("syd-mdwe".to_string()));

/// Holds a `String` to run `syd-ofd`.
/// Honours CARGO_BIN_EXE_syd-ofd environment variable.
pub static SYD_OFD: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-ofd").unwrap_or("syd-ofd".to_string()));

/// Holds a `String` to run `syd-pause`.
/// Honours CARGO_BIN_EXE_syd-pause environment variable.
pub static SYD_PAUSE: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-pause").unwrap_or("syd-pause".to_string()));

/// Holds a `String` to run `syd-pds`.
/// Honours CARGO_BIN_EXE_syd-pds environment variable.
pub static SYD_PDS: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-pds").unwrap_or("syd-pds".to_string()));

/// Holds a `String` to run `syd-sec`.
/// Honours CARGO_BIN_EXE_syd-sec environment variable.
pub static SYD_SEC: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-sec").unwrap_or("syd-sec".to_string()));

/// Holds a `String` to run `syd-size`.
/// Honours CARGO_BIN_EXE_syd-size environment variable.
pub static SYD_SIZE: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-size").unwrap_or("syd-size".to_string()));

/// Holds a `String` to run `syd-test-do`.
/// Honours CARGO_BIN_EXE_syd-test-do environment variable.
/// This path is a canonicalized for ease of use in sandboxing tests.
pub static SYD_DO: LazyLock<String> =
    LazyLock::new(|| match env::var("CARGO_BIN_EXE_syd-test-do") {
        Ok(var) => Path::new(&var)
            .canonicalize()
            .expect("CARGO_BIN_EXE_syd-test-do")
            .to_string_lossy()
            .into_owned(),
        Err(_) => which(if env::var("SYD_TEST_32").is_ok() {
            "syd-test-do32"
        } else {
            "syd-test-do"
        })
        .expect("syd-test-do"),
    });

/// Holds a `String` to run `syd-uts`.
/// Honours CARGO_BIN_EXE_syd-uts environment variable.
pub static SYD_UTS: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-uts").unwrap_or("syd-uts".to_string()));

/// Holds a `String` to run `syd-x`.
/// Honours CARGO_BIN_EXE_syd-x environment variable.
pub static SYD_X: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-x").unwrap_or("syd-x".to_string()));

#[cfg(feature = "oci")]
/// Holds a `String` to run `syd-oci`.
/// Honours CARGO_BIN_EXE_syd-oci environment variable.
pub static SYD_OCI: LazyLock<String> =
    LazyLock::new(|| env::var("CARGO_BIN_EXE_syd-oci").unwrap_or("syd-oci".to_string()));

/// A boolean which specifies if we're running under SourceHut CI.
pub static CI_BUILD: LazyLock<bool> = LazyLock::new(|| env::var("JOB_ID").ok().is_some());

/// A boolean which specifies if we're running under GitLab CI.
pub static GL_BUILD: LazyLock<bool> = LazyLock::new(|| env::var("CI_PROJECT_ID").ok().is_some());

/// A boolean which specifies whether standard error is a TTY.
#[allow(dead_code)]
pub static SYD_TEST_TTY: LazyLock<bool> =
    LazyLock::new(|| isatty(std::io::stderr()).unwrap_or(false));

pub struct Syd {
    cmd: Command,
    is_quiet: bool,
}

impl Syd {
    pub fn new(cmd: &str) -> Self {
        let mut cmd = Command::new(cmd);

        cmd.stdin(Stdio::null());

        Syd {
            cmd,
            is_quiet: false,
        }
    }

    pub fn c<S: ToString>(&mut self, arg: S) -> &mut Self {
        self.cmd.arg(format!("-c{}", arg.to_string()));
        self
    }

    pub fn m<S: ToString>(&mut self, arg: S) -> &mut Self {
        self.cmd.arg(format!("-m{}", arg.to_string()));
        self
    }

    pub fn p<S: ToString>(&mut self, arg: S) -> &mut Self {
        self.cmd.arg(format!("-p{}", arg.to_string()));
        self
    }

    #[expect(non_snake_case)]
    pub fn P<S: ToString>(&mut self, arg: S) -> &mut Self {
        self.cmd.arg(format!("-P{}", arg.to_string()));
        self
    }

    pub fn current_dir<S: ToString>(&mut self, arg: S) -> &mut Self {
        self.cmd.current_dir(arg.to_string());
        self
    }

    pub fn log(&mut self, value: &str) -> &mut Self {
        self.cmd
            .env(ENV_LOG, env::var(ENV_LOG).unwrap_or(value.to_string()));
        self
    }

    pub fn log_fd(&mut self, fd: RawFd) -> &mut Self {
        self.cmd.env(ENV_LOG_FD, fd.to_string());
        self
    }

    pub fn do_<I, S, V>(&mut self, value: V, args: I) -> &mut Self
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
        V: AsRef<OsStr>,
    {
        self.cmd.args(["--", &SYD_DO]);
        self.cmd.args(args);
        self.do__(value)
    }

    pub fn do__<V>(&mut self, value: V) -> &mut Self
    where
        V: AsRef<OsStr>,
    {
        self.cmd.env("SYD_TEST_DO", value);
        self
    }

    pub fn argv<I, S>(&mut self, args: I) -> &mut Self
    where
        I: IntoIterator<Item = S>,
        S: AsRef<OsStr>,
    {
        self.cmd.arg("--");
        self.cmd.args(args);
        self
    }

    pub fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
        self.cmd.arg(arg);
        self
    }

    pub fn args<I, S>(&mut self, args: I) -> &mut Self
    where
        I: IntoIterator<Item = S>,
        S: AsRef<std::ffi::OsStr>,
    {
        self.cmd.args(args);
        self
    }

    pub fn stdin(&mut self, cfg: Stdio) -> &mut Self {
        self.cmd.stdin(cfg);
        self
    }

    pub fn stdout(&mut self, cfg: Stdio) -> &mut Self {
        self.cmd.stdout(cfg);
        self
    }

    pub fn stderr(&mut self, cfg: Stdio) -> &mut Self {
        self.cmd.stderr(cfg);
        self
    }

    #[allow(dead_code)]
    pub fn quiet(&mut self) -> &mut Self {
        self.stderr(Stdio::null());
        self.stdout(Stdio::null());
        self.is_quiet = true;
        self
    }

    pub fn env<K, V>(&mut self, key: K, value: V) -> &mut Self
    where
        K: AsRef<OsStr>,
        V: AsRef<OsStr>,
    {
        self.cmd.env(key, value);
        self
    }

    pub fn env_remove<K>(&mut self, key: K) -> &mut Self
    where
        K: AsRef<OsStr>,
    {
        self.cmd.env_remove(key);
        self
    }

    pub fn spawn(&mut self) -> std::io::Result<Child> {
        if !self.is_quiet {
            eprintln!("\x1b[93m+ {:?}\x1b[0m", self.cmd);
        }
        self.cmd.spawn()
    }

    pub fn status(&mut self) -> std::io::Result<ExitStatus> {
        if !self.is_quiet {
            eprintln!("\x1b[93m+ {:?}\x1b[0m", self.cmd);
        }
        self.cmd.status()
    }

    pub fn output(&mut self) -> std::io::Result<Output> {
        if !self.is_quiet {
            eprintln!("\x1b[93m+ {:?}\x1b[0m", self.cmd);
        }

        // With `output` stderr defaults to `piped`.
        self.cmd.stderr(Stdio::inherit());

        self.cmd.output()
    }
}

/// Returns a `Command` to run `syd`.
/// Honours CARGO_BIN_EXE_syd environment variable.
pub fn syd() -> Syd {
    static USE_PERF: LazyLock<bool> = LazyLock::new(|| env::var_os("SYD_TEST_PERF").is_some());
    static USE_TRACE: LazyLock<bool> = LazyLock::new(|| env::var_os("SYD_TEST_TRACE").is_some());
    static USE_STRACE: LazyLock<bool> = LazyLock::new(|| env::var_os("SYD_TEST_STRACE").is_some());
    static USE_VALGRIND: LazyLock<bool> =
        LazyLock::new(|| env::var_os("SYD_TEST_VALGRIND").is_some());
    static USE_HELGRIND: LazyLock<bool> =
        LazyLock::new(|| env::var_os("SYD_TEST_HELGRIND").is_some());
    let mut cmd = Syd::new("timeout");
    if check_timeout_foreground() {
        // Use -v, not --verbose.
        // Latter is not supported by Android.
        cmd.arg("-v");
        cmd.arg("--foreground");
        cmd.arg("--preserve-status");
    }
    cmd.arg("-sKILL");
    cmd.arg(env::var("SYD_TEST_TIMEOUT").unwrap_or("15m".to_string()));
    if *USE_PERF {
        cmd.arg("perf");
        cmd.arg("record");
        cmd.arg("-F99");
        cmd.arg("--call-graph=dwarf");
        cmd.arg("-o/tmp/syd-perf.data"); // FIXME
        cmd.arg("--");
    } else if *USE_STRACE {
        cmd.arg("strace");
        cmd.arg("-yyY");

        if env::var_os("SYD_TEST_STRACE_NOFORK").is_none() {
            cmd.arg("-f");
        }

        if env::var_os("SYD_TEST_STRACE_VERBOSE").is_none() {
            cmd.arg("-s256");
            cmd.arg("-e!read,readv,write,writev");
        } else {
            cmd.arg("-s4096");
        }

        let arg = env::var_os("SYD_TEST_STRACE").unwrap();
        if !arg.is_empty() {
            cmd.arg(arg);
        }

        cmd.arg("--");
    } else if *USE_VALGRIND {
        cmd.arg("valgrind");
        cmd.arg("--leak-check=yes");
        cmd.arg("--track-origins=yes");
        cmd.arg("--track-fds=yes");
        cmd.arg("--trace-children=no");
        cmd.arg("--");
    } else if *USE_HELGRIND {
        cmd.arg("valgrind");
        cmd.arg("--tool=helgrind");
        cmd.arg("--");
    }
    cmd.arg(&*SYD);
    // To get meaningful panics:
    // 1. Set quick boot to on.
    // 2. Set as dumpable.
    cmd.arg("-q");
    cmd.m("trace/allow_unsafe_dumpable:1");
    // Set logging level to warning.
    // Allow user to override.
    cmd.env(ENV_LOG, env::var(ENV_LOG).unwrap_or("warn".to_string()));
    // Quiet TTY output to avoid noisy test logs.
    // Unless otherwise specified.
    if env::var_os(ENV_FORCE_TTY).is_none() {
        cmd.env(ENV_QUIET_TTY, "YesPlease");
    }
    // Allow coredumps and non-PIE.
    cmd.m("trace/allow_unsafe_exec_nopie:1"); // Allow non-PIE execution.
    cmd.m("trace/allow_unsafe_prlimit:1"); // Allow coredumps.
    if *USE_TRACE || (*USE_STRACE && env::var_os("SYD_TEST_STRACE_NOFORK").is_none()) {
        cmd.m("trace/allow_unsafe_ptrace:1"); // Disable ptrace.
    }
    cmd
}

/// Check if the given program is of GNU flavour.
pub fn is_program_gnu(command: &str) -> bool {
    if check_32bin_64host() {
        eprintln!("Binary/Host mismatch, cannot use program {command}!");
        return false;
    }

    // Check if `command --version` returns GNU substring.
    let out = Command::new(command)
        .arg("--version")
        .output()
        .unwrap()
        .stdout;
    memmem::find(&out, b"GNU").is_some()
}

/// 0. Check if there's a binary/host mismatch.
/// 1. Check if a program exists in PATH
pub fn is_program_available(command: &str) -> bool {
    if check_32bin_64host() {
        eprintln!("Binary/Host mismatch, cannot use program {command}!");
        return false;
    }

    // Check if the command exists in PATH
    Command::new("which")
        .stdout(Stdio::null())
        .arg(command)
        .status()
        .map(|status| status.success())
        .unwrap_or(false)
}

/// Resembles the `which` command, finds a program in PATH.
pub fn which(command: &str) -> Result<String, Errno> {
    let out = Command::new("which")
        .arg(command)
        .output()
        .expect("execute which")
        .stdout;
    if out.is_empty() {
        return Err(Errno::ENOENT);
    }

    let bin = String::from_utf8_lossy(&out);
    let bin = bin.trim();
    Ok(canonicalize(bin)
        .map_err(|_| Errno::last())?
        .to_string_lossy()
        .into_owned())
}

/// Check if IPv6 is supported
pub fn check_ipv6() -> bool {
    // Preliminary check for IPv6 availability
    let test_sock = SocketAddrV6::new(Ipv6Addr::LOCALHOST, 0, 0, 0);
    if let Err(error) = TcpListener::bind(test_sock) {
        eprintln!("IPv6 is not available on this system. Skipping test: {error}");
        false
    } else {
        true
    }
}

/// Checks if the C compiler allows nested routines.
/// Returns `true` if supported, `false` otherwise.
/// If successful, creates the executable `nested` in CWD.
/// See: https://gcc.gnu.org/onlinedocs/gcc/Nested-Functions.html
pub fn check_nested_routines() -> bool {
    let c_code = r#"
#include <stdlib.h>

int main(int argc, char *argv[])
{
    int x = atoi(argv[1]);
    int nested()
    {
        return x * x;
    };

    // Use pointer indirection so compiler cannot unnest.
    int (*fp)() = nested;

    // Yolo!
    return fp();
}
"#;
    if let Ok(mut file) = File::create("nested.c") {
        if file.write_all(c_code.as_bytes()).is_err() {
            return false;
        }
    } else {
        return false;
    }

    // Compile the C code using the C compiler
    let compile_status = Command::new("cc")
        .arg("-Wall")
        .arg("-Wextra")
        .arg("-zexecstack") // required on ppc64
        .arg("nested.c")
        .arg("-o")
        .arg("nested")
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status();

    // Return true if the compilation succeeded, false otherwise
    compile_status
        .map(|status| status.success())
        .unwrap_or(false)
}

/// Checks if the C compiler allows self-modifying code with mprotect.
/// Returns `true` if supported, `false` otherwise.
/// Only works on x86-64, always returns `false` on other architectures.
/// If successful, creates the executable `selfmod` in CWD.
/// See: https://shanetully.com/2013/12/writing-a-self-mutating-x86_64-c-program/
pub fn check_self_modifying_mp() -> bool {
    if !cfg!(target_arch = "x86_64") {
        return false;
    }

    let c_code = r#"
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/mman.h>

void foo(void);
int change_page_permissions_of_address(void *addr);

int main(void) {
    void *foo_addr = (void*)foo;

    // Change the permissions of the page that contains foo() to read,
    // write, and execute. This assumes that foo() is fully contained
    // by a single page.
    if(change_page_permissions_of_address(foo_addr) == -1) {
        int save_errno = errno;
        fprintf(stderr, "Error while changing page permissions of foo(): %s!\n", strerror(errno));
        return save_errno;
    }

    // Call the unmodified foo()
    puts("Calling foo...");
    foo();

    // Change the immediate value in the addl instruction in foo() to 42
    unsigned char *instruction = (unsigned char*)foo_addr + 18;
    *instruction = 0x2A;

    // Call the modified foo()
    puts("Calling foo...");
    foo();

    return 0;
}

void foo(void) {
    int i=0;
    i++;
    printf("i: %d\n", i);
}

int change_page_permissions_of_address(void *addr) {
    // Move the pointer to the page boundary
    int page_size = getpagesize();
    addr -= (unsigned long)addr % page_size;

    if(mprotect(addr, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) == -1) {
        return -1;
    }

    return 0;
}
"#;
    if let Ok(mut file) = File::create("selfmod.c") {
        if file.write_all(c_code.as_bytes()).is_err() {
            return false;
        }
    } else {
        return false;
    }

    // Compile the C code using the C compiler.
    let compile_status = Command::new("cc")
        .arg("-std=c99")
        .arg("-D_BSD_SOURCE")
        .arg("-Wall")
        .arg("selfmod.c")
        .arg("-o")
        .arg("selfmod")
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status();

    // Return true if the compilation succeeded, false otherwise.
    compile_status
        .map(|status| status.success())
        .unwrap_or(false)
}

/// Checks if the C compiler allows self-modifying code with executable stack.
/// Returns `true` if supported, `false` otherwise.
/// Only works on x86-64, always returns `false` on other architectures.
/// If successful, creates the executable `selfmod` in CWD.
/// See: https://shanetully.com/2013/12/writing-a-self-mutating-x86_64-c-program/
pub fn check_self_modifying_xs() -> bool {
    if !cfg!(target_arch = "x86_64") {
        return false;
    }

    let c_code = r#"
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <sys/mman.h>

void foo(void);
int change_page_permissions_of_address(void *addr);

int main(void) {
    void *foo_addr = (void*)foo;

    // Change the permissions of the page that contains foo() to read,
    // write, and execute. This assumes that foo() is fully contained
    // by a single page.
    if(change_page_permissions_of_address(foo_addr) == -1) {
        int save_errno = errno;
        fprintf(stderr, "Error while changing page permissions of foo(): %s!\n", strerror(errno));
        return save_errno;
    }

    // Call the unmodified foo()
    puts("Calling foo...");
    foo();

    // Change the immediate value in the addl instruction in foo() to 42
    unsigned char *instruction = (unsigned char*)foo_addr + 18;
    *instruction = 0x2A;

    // Call the modified foo()
    puts("Calling foo...");
    foo();

    return 0;
}

void foo(void) {
    int i=0;
    i++;
    printf("i: %d\n", i);
}

int change_page_permissions_of_address(void *addr) {
    // Move the pointer to the page boundary
    int page_size = getpagesize();
    addr -= (unsigned long)addr % page_size;

    if(mprotect(addr, page_size, PROT_READ | PROT_WRITE | PROT_EXEC) == -1) {
        return -1;
    }

    return 0;
}
"#;
    if let Ok(mut file) = File::create("selfmod.c") {
        if file.write_all(c_code.as_bytes()).is_err() {
            return false;
        }
    } else {
        return false;
    }

    // Compile the C code using the C compiler.
    let compile_status = Command::new("cc")
        .arg("-std=c99")
        .arg("-D_BSD_SOURCE")
        .arg("-zexecstack")
        .arg("-Wall")
        .arg("selfmod.c")
        .arg("-o")
        .arg("selfmod")
        .stdout(Stdio::inherit())
        .stderr(Stdio::inherit())
        .status();

    // Return true if the compilation succeeded, false otherwise.
    compile_status
        .map(|status| status.success())
        .unwrap_or(false)
}

/// Format a `Duration` into a human readable `String`.
pub fn format_duration(d: Duration) -> String {
    let total_seconds = d.as_secs();
    let hours = total_seconds / 3600;
    let minutes = (total_seconds % 3600) / 60;
    let seconds = total_seconds % 60;

    format!("{hours}h {minutes}m {seconds}s")
}

/// Checks if the current running binary is 32-bit and the host system is 64-bit.
pub fn check_32bin_64host() -> bool {
    // Check if the current binary is 32-bit using a compile-time constant.
    // This constant is set by the Cargo build script based on the target architecture.
    #[cfg(target_pointer_width = "32")]
    let is_binary_32bit = true;
    #[cfg(not(target_pointer_width = "32"))]
    let is_binary_32bit = false;

    // Use uname to check if the host is 64-bit.
    let arch = uname()
        .expect("uname")
        .machine()
        .to_string_lossy()
        .to_string();

    if is_binary_32bit && arch.contains("64") {
        eprintln!("32->64: Running 32bit on {arch}!");
        true
    } else {
        false
    }
}

/// Checks whether strace supports syscall injection.
pub fn check_strace_inject() -> bool {
    let status = Command::new("strace")
        .arg("-qq")
        .arg("-e")
        .arg("trace=getpid")
        .arg("-e")
        .arg("inject=getpid:retval=2525")
        .arg("--")
        .arg("sh")
        .arg("-c")
        .arg("test $$ -eq 2525")
        .status();

    match status {
        Ok(status) => status.success(),
        Err(_) => false,
    }
}

/// Enable coredumps.
pub fn enable_coredumps() -> Result<(), Errno> {
    // Set both the soft and hard limits
    confine_rlimit(Resource::RLIMIT_CORE, Some(RLIM_INFINITY))
}

/// Check if timeout --foreground is supported.
pub fn check_timeout_foreground() -> bool {
    Command::new("timeout")
        .arg("--foreground")
        .arg("-sKILL")
        .arg("60s")
        .arg("true")
        .stdout(Stdio::null())
        .stderr(Stdio::null())
        .status()
        .map(|status| status.success())
        .unwrap_or(false)
}

/// Check if the `ip` command is of `iproute2` (not `busybox`).
pub fn check_iproute2() -> bool {
    Command::new("sh")
        .arg("-cex")
        .arg("ip -V | grep -iq iproute2")
        .status()
        .map(|status| status.success())
        .unwrap_or(false)
}

/// Retrieves the current directory or its basename as a `PathBuf`, based on the `base` parameter.
pub fn current_dir(base: bool) -> std::io::Result<PathBuf> {
    // Get the current working directory
    let current_dir = env::current_dir()?;

    if base {
        // Extract the basename
        let basename = current_dir
            .file_name()
            .ok_or(std::io::Error::other("Failed to get the basename"))
            .map(PathBuf::from)?;

        Ok(basename)
    } else {
        // Return the full path as PathBuf
        current_dir.canonicalize()
    }
}

/// Shuffles a vector using the Fisher-Yates algorithm,
/// utilizing random numbers obtained via `libc::rand`.
/// On Android, falls back to `syd::rng::randint`.
///
/// # Arguments
/// * `vec` - A mutable reference to the vector to shuffle.
pub fn shuffle_vec<T>(vec: &mut [T]) {
    let len = vec.len();
    for i in 0..len {
        // Android does not define libc::rand().
        #[cfg(not(target_os = "android"))]
        let r = unsafe { libc::rand() } as usize;
        #[cfg(target_os = "android")]
        let r = syd::rng::randint(usize::MIN..=usize::MAX).unwrap();

        let j = r % (len - i) + i;
        vec.swap(i, j); // Perform the swap.
    }
}
