//
// Syd: rock-solid unikernel
// src/proc.rs: /proc utilities
//
// Copyright (c) 2023, 2024 Ali Polatel <alip@chesswob.org>
// Based in part upon procps-ng's library/devname.c which is:
//   Copyright © 2011-2023 Jim Warner <james.warner@comcast.net>
//   Copyright © 2017-2023 Craig Small <csmall@dropbear.xyz>
//   Copyright © 1998-2009 Albert Cahalan
//
// SPDX-License-Identifier: GPL-3.0-or-later

// SAFETY: PathBuf is used in proc_task_limit() only.
// TODO: Drop PathBuf usage from proc_task_limit()!
#[allow(clippy::disallowed_types)]
use std::{
    ffi::OsStr,
    fs::{read_dir, read_to_string, File},
    io::Read,
    ops::Range,
    os::{fd::RawFd, unix::ffi::OsStrExt},
    path::{Path, PathBuf},
    str::FromStr,
};
use std::{ffi::OsString, os::unix::ffi::OsStringExt};

use indexmap::IndexSet;
use libseccomp::ScmpArch;
use nix::{errno::Errno, libc::pid_t, sys::stat::Mode, unistd::Pid};
use procfs::process::{MMPermissions, MMapPath, MemoryMap, Process};
use rustc_hash::FxHashSet;
use serde::{Serialize, Serializer};

use crate::{
    fs::{mask_path, read_link, SmallPathBuf, TinyPathBuf},
    syscall_instruction_size,
};

const TGID_PREFIX: &[u8] = b"Tgid:\t";
const UMASK_PREFIX: &[u8] = b"Umask:\t";

// major 204 is a mess -- "Low-density serial ports"
const LOW_DENSITY_NAMES: &[&str] = &[
    "LU0", "LU1", "LU2", "LU3", "FB0", "SA0", "SA1", "SA2", "SC0", "SC1", "SC2", "SC3", "FW0",
    "FW1", "FW2", "FW3", "AM0", "AM1", "AM2", "AM3", "AM4", "AM5", "AM6", "AM7", "AM8", "AM9",
    "AM10", "AM11", "AM12", "AM13", "AM14", "AM15", "DB0", "DB1", "DB2", "DB3", "DB4", "DB5",
    "DB6", "DB7", "SG0", "SMX0", "SMX1", "SMX2", "MM0", "MM1", "CPM0", "CPM1", "CPM2",
    "CPM3", /* "CPM4", "CPM5", */
    // bad allocation?
    "IOC0", "IOC1", "IOC2", "IOC3", "IOC4", "IOC5", "IOC6", "IOC7", "IOC8", "IOC9", "IOC10",
    "IOC11", "IOC12", "IOC13", "IOC14", "IOC15", "IOC16", "IOC17", "IOC18", "IOC19", "IOC20",
    "IOC21", "IOC22", "IOC23", "IOC24", "IOC25", "IOC26", "IOC27", "IOC28", "IOC29", "IOC30",
    "IOC31", "VR0", "VR1", "IOC84", "IOC85", "IOC86", "IOC87", "IOC88", "IOC89", "IOC90", "IOC91",
    "IOC92", "IOC93", "IOC94", "IOC95", "IOC96", "IOC97", "IOC98", "IOC99", "IOC100", "IOC101",
    "IOC102", "IOC103", "IOC104", "IOC105", "IOC106", "IOC107", "IOC108", "IOC109", "IOC110",
    "IOC111", "IOC112", "IOC113", "IOC114", "IOC115", "SIOC0", "SIOC1", "SIOC2", "SIOC3", "SIOC4",
    "SIOC5", "SIOC6", "SIOC7", "SIOC8", "SIOC9", "SIOC10", "SIOC11", "SIOC12", "SIOC13", "SIOC14",
    "SIOC15", "SIOC16", "SIOC17", "SIOC18", "SIOC19", "SIOC20", "SIOC21", "SIOC22", "SIOC23",
    "SIOC24", "SIOC25", "SIOC26", "SIOC27", "SIOC28", "SIOC29", "SIOC30", "SIOC31", "PSC0", "PSC1",
    "PSC2", "PSC3", "PSC4", "PSC5", "AT0", "AT1", "AT2", "AT3", "AT4", "AT5", "AT6", "AT7", "AT8",
    "AT9", "AT10", "AT11", "AT12", "AT13", "AT14", "AT15", "NX0", "NX1", "NX2", "NX3", "NX4",
    "NX5", "NX6", "NX7", "NX8", "NX9", "NX10", "NX11", "NX12", "NX13", "NX14", "NX15",
    "J0", // minor is 186
    "UL0", "UL1", "UL2", "UL3", "xvc0", // FAIL -- "/dev/xvc0" lacks "tty" prefix
    "PZ0", "PZ1", "PZ2", "PZ3", "TX0", "TX1", "TX2", "TX3", "TX4", "TX5", "TX6", "TX7", "SC0",
    "SC1", "SC2", "SC3", "MAX0", "MAX1", "MAX2", "MAX3",
];

/// Retrieves the thread group ID (TGID) for the specified thread ID (TID).
pub fn proc_tgid(tid: Pid) -> Result<Pid, Errno> {
    // Construct path to the appropriate status file
    let mut buf = itoa::Buffer::new();
    let mut proc = TinyPathBuf::new();
    proc.push(Path::new("/proc"));
    proc.push(Path::new(buf.format(tid.as_raw())));
    proc.push(Path::new("status"));

    // Open the file
    let mut file = File::open(proc).or(Err(Errno::ESRCH))?;
    let mut buf = [0; 84]; // Read the first 84 bytes where the tgid is likely to be.
    let bytes_read = file.read(&mut buf).or(Err(Errno::ESRCH))?;

    // Search for "Tgid:" pattern directly in bytes
    if let Some(position) = buf[..bytes_read]
        .windows(TGID_PREFIX.len())
        .position(|window| window == TGID_PREFIX)
    {
        let start = position.saturating_add(TGID_PREFIX.len());
        // Find the end of the tgid value assuming it ends with a newline.
        if let Some(end) = buf[start..].iter().position(|&b| b == b'\n') {
            // Parse tgid directly from bytes
            let mut tgid: pid_t = 0;
            for &digit in &buf[start..start.saturating_add(end)] {
                tgid = tgid
                    .saturating_mul(10)
                    .saturating_add(pid_t::from(digit.saturating_sub(b'0')));
            }
            return Ok(Pid::from_raw(tgid));
        }
    }

    Err(Errno::ESRCH)
}

/// Get the umask of the given `Pid`.
pub(crate) fn proc_umask(pid: Pid) -> Result<Mode, Errno> {
    // Construct path to the appropriate status file
    let mut buf = itoa::Buffer::new();
    let mut proc = TinyPathBuf::new();
    proc.push(Path::new("/proc"));
    proc.push(Path::new(buf.format(pid.as_raw())));
    proc.push(Path::new("status"));

    // Open the file
    let mut file = File::open(proc).or(Err(Errno::ESRCH))?;
    let mut buf = [0u8; 42]; // Read the first 42 bytes where the umask is likely to be.
    let bytes_read = file.read(&mut buf).or(Err(Errno::ESRCH))?;

    // Search for "Umask:" pattern directly in bytes
    if let Some(position) = buf[..bytes_read]
        .windows(UMASK_PREFIX.len())
        .position(|window| window == UMASK_PREFIX)
    {
        let start = position.saturating_add(UMASK_PREFIX.len());
        // Find the end of the umask value assuming it ends with a newline.
        if let Some(end) = buf[start..].iter().position(|&b| b == b'\n') {
            // Parse umask directly from bytes
            let mut umask = 0u32;
            for &digit in &buf[start..start.saturating_add(end)] {
                umask = umask
                    .saturating_mul(8)
                    .saturating_add(u32::from(digit.saturating_sub(b'0')));
            }
            return Ok(Mode::from_bits_truncate(umask));
        }
    }

    Err(Errno::EIO)
}

/// Retrieve the system pipe max limit.
pub fn proc_pipemax() -> Result<nix::libc::c_int, Errno> {
    // Open the file
    let mut file = File::open("/proc/sys/fs/pipe-max-size").or(Err(Errno::ENOENT))?;

    // Read up to 24 bytes.
    let mut buf = [0u8; 24];
    let bytes_read = file.read(&mut buf).or(Err(Errno::EINVAL))?;

    // Find the position of the newline character.
    if let Some(end) = buf[..bytes_read].iter().position(|&b| b == b'\n') {
        // Parse the size directly from bytes.
        let mut size: nix::libc::c_int = 0;
        for &digit in &buf[..end] {
            size = size
                .saturating_mul(10)
                .saturating_add(nix::libc::c_int::from(digit.saturating_sub(b'0')));
        }
        return Ok(size);
    }

    Err(Errno::EINVAL)
}

/// Retrieves the command line of the specified process ID (PID)
/// concatenated as a single string.
///
/// This function reads the `/proc/<pid>/cmdline` file and concatenates
/// the arguments using spaces. The function takes care of replacing null
/// bytes (`'\0'`) with spaces to format the command line as a readable string.
///
/// # Arguments
///
/// * `pid` - The process ID for which to retrieve the command line.
///
/// # Returns
///
/// * `Ok(PathBuf)` - The command line of the process as a single string.
/// * `Err(Errno)`  - An error, depending on the issue encountered while reading
///                   or processing the cmdline file.
///
/// # Examples
///
/// ```rust
/// use nix::unistd::Pid;
/// use syd::proc::proc_cmdline;
///
/// let cmdline = proc_cmdline(Pid::this());
/// assert!(cmdline.is_ok());
/// ```
#[cfg(feature = "log")]
pub fn proc_cmdline(pid: Pid) -> Result<PathBuf, Errno> {
    const LIMIT: usize = 256;

    // Construct path to the appropriate cmdline file.
    let mut buf = itoa::Buffer::new();
    let mut cmd = TinyPathBuf::new();
    cmd.push(Path::new("/proc"));
    cmd.push(Path::new(buf.format(pid.as_raw())));
    cmd.push(Path::new("cmdline"));

    let mut file = File::open(cmd).or(Err(Errno::ENOENT))?;
    let mut buf = vec![0u8; LIMIT];
    let bytes_read = file.read(&mut buf).or(Err(Errno::ENOENT))?;

    // Determine if EOF was reached or if we hit the limit
    if bytes_read <= 1 {
        // Empty cmdline.
        return Ok(PathBuf::new());
    } else if bytes_read >= LIMIT {
        // Check if the last byte read is not a null byte,
        // indicating there's more data.
        if buf[LIMIT - 1] != 0 {
            // Append ellipsis to indicate truncation.
            buf.extend_from_slice("…".as_bytes());
        } else {
            // Remove the NUL-byte.
            buf.pop();
        }
    } else {
        // If EOF was hit before the limit,
        // resize the buffer to bytes_read - 1 (for null byte).
        buf.resize(bytes_read - 1, 0);
    }

    // Replace null bytes with spaces
    for byte in &mut buf {
        if *byte == 0 {
            *byte = b' ';
        }
    }

    Ok(OsString::from_vec(buf).into())
}

/// Retrieves the command name (comm) of the specified process ID (PID)
/// as a single string.
///
/// This function reads the `/proc/<pid>/comm` file. It reads up to 16 characters,
/// which is typically sufficient for process names.
///
/// # Arguments
///
/// * `pid` - The process ID for which to retrieve the command name.
///
/// # Returns
///
/// * `Ok(PathBuf)` - The command name of the process as a single string.
/// * `Err(Errno)`  - An error, depending on the issue encountered while reading
///                   the comm file.
///
/// # Examples
///
/// ```rust
/// use nix::unistd::Pid;
/// use syd::proc::proc_comm;
///
/// let comm = proc_comm(Pid::this());
/// assert!(comm.is_ok());
/// ```
#[allow(clippy::disallowed_types)]
#[cfg(not(feature = "log"))]
pub fn proc_comm(pid: Pid) -> Result<PathBuf, Errno> {
    const LIMIT: usize = 16;
    let mut buf = itoa::Buffer::new();
    let mut path = TinyPathBuf::new();
    path.push(Path::new("/proc"));
    path.push(Path::new(buf.format(pid.as_raw())));
    path.push(Path::new("comm"));

    let mut file = File::open(path).or(Err(Errno::ESRCH))?;
    let mut data = Vec::with_capacity(LIMIT);

    // Read up to LIMIT characters or until EOF
    file.read_to_end(&mut data).or(Err(Errno::ESRCH))?;

    // Remove the NUL-byte.
    data.pop();

    Ok(OsString::from_vec(data).into())
}

/// Retrieves the current working directory (CWD) of the specified process ID (PID).
///
/// This function reads the symbolic link `/proc/<pid>/cwd` to determine the CWD.
///
/// # Arguments
///
/// * `pid` - The process ID for which to retrieve the current working directory.
///
/// # Returns
///
/// * `Ok(SmallPathBuf)` - The current working directory of the process.
/// * `Err(Errno)`       - An error, depending on the issue encountered while reading
///                        or resolving the `cwd` symlink.
///
/// # Examples
///
/// ```rust
/// use nix::unistd::Pid;
/// use syd::proc::proc_cwd;
///
/// let cwd = proc_cwd(Pid::this());
/// assert!(cwd.is_ok());
/// ```
pub fn proc_cwd(pid: Pid) -> Result<SmallPathBuf, Errno> {
    let mut buf = itoa::Buffer::new();
    let mut path = TinyPathBuf::new();
    path.push(Path::new("/proc"));
    path.push(Path::new(buf.format(pid.as_raw())));
    path.push(Path::new("cwd"));
    read_link(&path).or(Err(Errno::ENOENT))
}

/// Returns the process group ID of the process.
pub fn proc_pgrp(pid: Pid) -> Result<Pid, Errno> {
    Ok(Pid::from_raw(
        Process::new(pid.as_raw())
            .or(Err(Errno::ESRCH))?
            .stat()
            .or(Err(Errno::EPERM))?
            .pgrp,
    ))
}

/// Reads the tty number from /proc/[pid]/stat and figures out the corresponding /dev/tty device node path.
#[allow(clippy::arithmetic_side_effects)]
#[allow(clippy::cast_sign_loss)]
#[allow(clippy::disallowed_methods)]
pub fn proc_tty(pid: Pid) -> Result<SmallPathBuf, Errno> {
    let stat = Process::new(pid.as_raw())
        .or(Err(Errno::ESRCH))?
        .stat()
        .or(Err(Errno::EPERM))?;
    if stat.tty_nr <= 0 {
        // Process has no controlling terminal
        return Err(Errno::ENXIO);
    }
    let (major, minor) = stat.tty_nr();
    match major {
        3 => Ok(SmallPathBuf::from(format!(
            "/dev/tty{}{}",
            "pqrstuvwxyzabcde"[(minor >> 4) as usize..]
                .chars()
                .next()
                .unwrap(),
            "0123456789abcdef"[(minor & 0x0f) as usize..]
                .chars()
                .next()
                .unwrap()
        ))),
        4 => {
            if minor < 64 {
                Ok(SmallPathBuf::from(format!("/dev/tty{}", minor)))
            } else {
                Ok(SmallPathBuf::from(format!("/dev/ttyS{}", minor - 64)))
            }
        }
        11 => Ok(SmallPathBuf::from(format!("/dev/ttyB{}", minor))),
        14 => Ok(SmallPathBuf::from(format!("/dev/tty{}", minor))), // Standard TTYs
        17 => Ok(SmallPathBuf::from(format!("/dev/ttyH{}", minor))),
        19 | 22 | 23 => Ok(SmallPathBuf::from(format!("/dev/ttyD{}", minor))),
        24 => Ok(SmallPathBuf::from(format!("/dev/ttyE{}", minor))),
        32 => Ok(SmallPathBuf::from(format!("/dev/ttyX{}", minor))),
        43 => Ok(SmallPathBuf::from(format!("/dev/ttyI{}", minor))),
        46 => Ok(SmallPathBuf::from(format!("/dev/ttyR{}", minor))),
        48 => Ok(SmallPathBuf::from(format!("/dev/ttyL{}", minor))),
        57 => Ok(SmallPathBuf::from(format!("/dev/ttyP{}", minor))),
        71 => Ok(SmallPathBuf::from(format!("/dev/ttyF{}", minor))),
        75 => Ok(SmallPathBuf::from(format!("/dev/ttyW{}", minor))),
        78 | 112 => Ok(SmallPathBuf::from(format!("/dev/ttyM{}", minor))),
        105 => Ok(SmallPathBuf::from(format!("/dev/ttyV{}", minor))),
        136..=143 => Ok(SmallPathBuf::from(format!(
            "/dev/pts/{}",
            minor + (major - 136) * 256
        ))),
        148 => Ok(SmallPathBuf::from(format!("/dev/ttyT{}", minor))),
        154 | 156 => Ok(SmallPathBuf::from(format!(
            "/dev/ttySR{}",
            minor + if major == 156 { 256 } else { 0 }
        ))),
        164 => Ok(SmallPathBuf::from(format!("/dev/ttyCH{}", minor))),
        166 => Ok(SmallPathBuf::from(format!("/dev/ttyACM{}", minor))),
        172 => Ok(SmallPathBuf::from(format!("/dev/ttyMX{}", minor))),
        174 => Ok(SmallPathBuf::from(format!("/dev/ttySI{}", minor))),
        188 => Ok(SmallPathBuf::from(format!("/dev/ttyUSB{}", minor))),
        204 => {
            if minor as usize >= LOW_DENSITY_NAMES.len() {
                Err(Errno::ENXIO)
            } else {
                Ok(SmallPathBuf::from(format!(
                    "/dev/tty{}",
                    LOW_DENSITY_NAMES[minor as usize]
                )))
            }
        }
        208 => Ok(SmallPathBuf::from(format!("/dev/ttyU{}", minor))),
        216 => Ok(SmallPathBuf::from(format!("/dev/ttyUB{}", minor))),
        224 => Ok(SmallPathBuf::from(format!("/dev/ttyY{}", minor))),
        227 => Ok(SmallPathBuf::from(format!("/dev/3270/tty{}", minor))),
        229 => Ok(SmallPathBuf::from(format!("/dev/iseries/vtty{}", minor))),
        256 => Ok(SmallPathBuf::from(format!("/dev/ttyEQ{}", minor))),
        _ => Err(Errno::ENXIO),
    }
}

/// Checks if the number of tasks across all processes in the system
/// exceeds the given limit by inspecting the `/proc` filesystem.
///
/// This function iterates over each entry in the `/proc` directory,
/// assuming each numeric directory name represents a process ID. For
/// each process, it counts the number of tasks (threads) by counting
/// entries in the `/proc/[pid]/task` directory. The function skips
/// over the initial entry `/proc/1`.
///
/// # Returns
/// `Ok(true)`: Number of tasks reach the given limit.
/// `Err(Errno)`: An error occurred during the operation, excluding
/// ENOENT (file not found), EACCES (access denied), EPERM (permission
/// denied), and ESRCH (process not found) errors, which are ignored.
///
/// # Errors
/// This function returns an error for any issue encountered while
/// reading the `/proc` directory or task subdirectories, except for
/// ENOENT, EACCES, EPERM, and ESRCH errors, which are ignored. The
/// error is wrapped in an `Errno` type.
#[allow(clippy::disallowed_types)]
pub fn proc_task_limit(max: usize) -> Result<bool, Errno> {
    let proc_entries = read_dir("/proc").or(Err(Errno::ENOENT))?;

    let result = proc_entries
        .filter_map(Result::ok)
        .filter(|entry| entry.file_name().as_bytes() != b"1" && is_numeric(&entry.file_name()))
        .try_fold(
            (0usize, Vec::<PathBuf>::new()),
            |(count, mut entries), entry| {
                let count = count.saturating_add(1);
                if count >= max {
                    // Early termination with Err to break out of the try_fold
                    return Err(());
                }
                entries.push(entry.path());
                Ok((count, entries))
            },
        );

    // Unwrap the result of try_fold, handling early termination
    let (mut pid_count, eligible_entries) = match result {
        Ok(data) => data,
        Err(_) => return Ok(true),
    };

    // Step 2: Count tasks in each PID
    for path in eligible_entries {
        match read_dir(path.join("task")) {
            Ok(tasks) => {
                pid_count = pid_count.saturating_add(tasks.count()).saturating_sub(1);
                if pid_count >= max {
                    return Ok(true);
                }
            }
            Err(error) => match error.raw_os_error() {
                Some(
                    nix::libc::ENOENT | nix::libc::EPERM | nix::libc::EACCES | nix::libc::ESRCH,
                ) => {}
                Some(error) => return Err(Errno::from_raw(error)),
                None => return Err(Errno::ENOENT),
            },
        }
    }

    Ok(false)
}

#[inline]
fn is_numeric(name: &OsStr) -> bool {
    name.as_bytes().iter().all(|&b| b.is_ascii_digit())
}

const FD: &[u8] = b"/fd/";
const PROC: &[u8] = b"/proc/";
const TASK: &[u8] = b"/task/";

/// 0. Assumes the given path is normalized.
/// 1. The path must start with /proc.
/// 2. The second component must be a numeric PID equal to the given PID.
/// 3. An optional task/[TID] part, where [TID] is a numeric PID.
/// 4. Finally, it checks for an fd component followed by a numeric file descriptor.
///    Note, This function does not allocate.
///    Note, use negated PID if you only need validation and not the exact FD value.
///
/// SAFETY: This function returns Err(Errno::ELOOP) if PID is present but not equal to given PID.
#[allow(clippy::cognitive_complexity)]
pub fn proc_fd(pid: Pid, path: &Path) -> Result<Option<RawFd>, Errno> {
    let pid = pid.as_raw();
    #[allow(clippy::arithmetic_side_effects)]
    let (pid, need_fd) = if pid < 0 { (-pid, false) } else { (pid, true) };

    let path = path.as_os_str().as_bytes();
    if !path.starts_with(PROC) {
        return Ok(None);
    }

    let path_without_proc = &path[PROC.len()..];
    let next_slash_index = path_without_proc
        .iter()
        .position(|&b| b == b'/')
        .unwrap_or(path_without_proc.len());
    let pid_section = &path_without_proc[..next_slash_index];
    if pid_section.is_empty() || !pid_section[0].is_ascii_digit() {
        return Ok(None); // Skip /proc/not-a-pid paths.
    }

    let after_pid_section = &path_without_proc[next_slash_index..];
    let start_of_interesting_part = if after_pid_section.starts_with(TASK) {
        let after_task_section = &after_pid_section[TASK.len()..];
        after_task_section
            .iter()
            .position(|&b| b == b'/')
            .map_or(after_task_section.len(), |p| p.saturating_add(TASK.len()))
    } else {
        0
    };

    let remaining_path = &after_pid_section[start_of_interesting_part..];
    if remaining_path.starts_with(FD) {
        // Check for valid file descriptor number after /fd/
        let fd_section = &remaining_path[FD.len()..];
        if fd_section.is_empty() {
            // Path ends with /fd, indicating it's a directory listing request, not an FD.
            return Ok(None);
        }

        if bytes_to_pid(pid_section) != pid {
            return Err(Errno::ELOOP); // PID mismatch detected.
        }

        let fd = if need_fd { bytes_to_fd(fd_section) } else { 0 };
        return Ok(Some(fd));
    } else if matches!(remaining_path, b"/cwd" | b"/exe" | b"/root") {
        // Validate PID only for special paths to avoid rejecting legitimate queries to other PIDs.
        if bytes_to_pid(pid_section) == pid {
            return Ok(Some(nix::libc::AT_FDCWD));
        } else {
            return Err(Errno::ELOOP);
        }
    }

    // No specific handling required, PID validation not necessary for paths not ending with /fd or special.
    Ok(None)
}

#[inline]
fn bytes_to_pid(bytes: &[u8]) -> pid_t {
    bytes.iter().fold(0, |acc, &b| {
        acc.saturating_mul(10)
            .saturating_add(pid_t::from(b.saturating_sub(b'0')))
    })
}

#[inline]
fn bytes_to_fd(bytes: &[u8]) -> RawFd {
    bytes.iter().fold(0, |acc, &b| {
        acc.saturating_mul(10)
            .saturating_add(RawFd::from(b.saturating_sub(b'0')))
    })
}

/// Checks whether process memory usage is within the give maximum.
///
/// This function uses the `procfs` crate to obtain detailed memory maps
/// from `/proc/[pid]/smaps`. It sums multiple memory usage values reported in these maps
/// to calculate a more comprehensive total memory usage.
///
/// # Arguments
///
/// * `process` - `Process` instance representing the process.
///
/// # Returns
///
/// This function returns a `Result<bool, Errno>`. It returns Ok(true)
/// if the limit was exceeded Ok(false) otherwise. On failure, it
/// returns `Errno`.
///
/// # Errors
///
/// This function returns an error if it fails to retrieve the process's memory maps,
/// typically due to insufficient permissions or an invalid process ID.
pub fn proc_mem_limit(process: &Process, max: u64) -> Result<bool, Errno> {
    match process.smaps() {
        Ok(maps) => {
            let mut total_size: u64 = 0;
            for map in &maps.0 {
                match &map.pathname {
                    MMapPath::Path(_)
                    | MMapPath::Anonymous
                    | MMapPath::Stack
                    | MMapPath::Other(_) => {
                        let pss = map.extension.map.get("Pss").copied().unwrap_or(0);
                        let private_dirty =
                            map.extension.map.get("Private_Dirty").copied().unwrap_or(0);
                        let shared_dirty =
                            map.extension.map.get("Shared_Dirty").copied().unwrap_or(0);

                        total_size = total_size.saturating_add(
                            pss.saturating_add(private_dirty)
                                .saturating_add(shared_dirty),
                        );

                        // Stop processing if total size exceeds or equals max
                        if total_size >= max {
                            return Ok(true);
                        }
                    }
                    _ => (),
                }
            }

            // If we're at this point, we did not hit the limit.
            Ok(false)
        }
        Err(_) => Err(Errno::last()),
    }
}

/// Describe the given syscall instruction by looking it up with the
/// given address in /proc/pid/maps. Returns None on errors.
pub fn proc_mmap(pid: Pid, arch: ScmpArch, addr: u64) -> Option<SydMMap> {
    let maps = Process::new(pid.as_raw()).ok()?.maps().ok()?;

    // Iterate over each memory map entry.
    for mmap in maps.0 {
        // Check if the map has executable permissions.
        if mmap.perms.contains(MMPermissions::EXECUTE) {
            // Check if the address falls within range.
            if (mmap.address.0..mmap.address.1).contains(&addr) {
                return Some(SydMMap(mmap, addr, syscall_instruction_size(arch)));
            }
        }
    }

    None
}

/// Wraps the data type MemoryMap, the instruction pointer and the
/// architecture-dependent syscall instruction size, and provides
/// serialization.
pub struct SydMMap(pub MemoryMap, pub u64, pub u64);

impl Serialize for SydMMap {
    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
    where
        S: Serializer,
    {
        let offset = self
            .0
            .offset
            .saturating_add(self.1.saturating_sub(self.0.address.0))
            .saturating_sub(self.2);
        let memory = match &self.0.pathname {
            MMapPath::Path(path) => mask_path(path),
            MMapPath::Heap => "[heap]".to_string(),
            MMapPath::Stack => "[stack]".to_string(),
            MMapPath::TStack(tid) => format!("[tstack:tid={tid}]"),
            MMapPath::Vdso => "[vdso]".to_string(),
            MMapPath::Vvar => "[vvar]".to_string(),
            MMapPath::Vsyscall => "[vsyscall]".to_string(),
            MMapPath::Rollup => "[rollup]".to_string(),
            MMapPath::Anonymous => "[anonymous]".to_string(),
            MMapPath::Vsys(key) => format!("[vsys:key={key:x}]"),
            MMapPath::Other(name) => name.to_string(),
        };

        serializer.serialize_str(&format!("{memory}+{offset:x}"))
    }
}

/// Collects all unique paths with executable permissions from the
/// memory maps of a process.
///
/// # Arguments
///
/// * `pid` - The process ID of the target process.
///
/// # Returns
///
/// A `Result` containing a vector of `(SmallPathBuf,u64,i32,i32)` objects
/// for all unique paths with executable permissions along with their
/// device ID and inode. On error, it returns an `Errno`.
#[allow(clippy::type_complexity)]
pub fn proc_executables(pid: Pid) -> Result<Vec<(SmallPathBuf, u64, i32, i32)>, Errno> {
    let maps = Process::new(pid.as_raw())
        .or(Err(Errno::ESRCH))?
        .maps()
        .or(Err(Errno::EPERM))?;

    // Create an IndexSet to store unique executable paths while
    // preserving insertion order.
    let mut paths = IndexSet::new();

    // Iterate over each memory map entry.
    for map in maps.0 {
        // Match on the pathname to extract the path.
        if let MMapPath::Path(path) = map.pathname {
            // Check if the map has executable permissions.
            if map.perms.contains(MMPermissions::EXECUTE) {
                // Insert the path into the IndexSet.
                paths.insert((path.into(), map.inode, map.dev.0, map.dev.1));
            }
        }
    }

    // Convert the IndexSet to a vector and return.
    Ok(paths.into_iter().collect())
}

/// Locate stack and return the stack memory address range.
///
/// # Arguments
///
/// * `pid` - The process ID of the target process.
///
/// # Returns
///
/// A range on success, on error, it returns an `Errno`.
pub fn proc_stack(pid: Pid) -> Result<Range<u64>, Errno> {
    let maps = Process::new(pid.as_raw())
        .or(Err(Errno::ESRCH))?
        .maps()
        .or(Err(Errno::EPERM))?;

    // Iterate over each memory map entry.
    for map in maps.0 {
        if let MMapPath::Stack = map.pathname {
            return Ok(map.address.0..map.address.1);
        }
    }

    Err(Errno::ENOENT)
}

/// Retrieves the current stack pointer (rsp) for the specified process ID (pid).
/// If the process is "running", it returns `Errno::EBUSY`.
pub fn proc_stack_pointer(pid: Pid) -> Result<u64, Errno> {
    // Construct path to the appropriate syscall file.
    let mut buf = itoa::Buffer::new();
    let mut proc = TinyPathBuf::new();
    proc.push(Path::new("/proc"));
    proc.push(Path::new(buf.format(pid.as_raw())));
    proc.push(Path::new("syscall"));

    let mut file = File::open(&proc).or(Err(Errno::ESRCH))?;
    let mut data = Vec::with_capacity(128);
    file.read_to_end(&mut data).or(Err(Errno::EIO))?;

    // Check if the contents equal "running".
    if data.starts_with(b"running") {
        return Err(Errno::EBUSY);
    }

    // Split the contents by whitespace.
    let parts: Vec<&[u8]> = data.split(|&b| b == b' ').collect();
    if parts.len() >= 2 {
        // Convert the rsp value from hex to u64.
        #[allow(clippy::arithmetic_side_effects)]
        let rsp = parts[parts.len() - 2];
        if let Some(rsp) = rsp.strip_prefix(b"0x") {
            if let Ok(rsp) =
                u64::from_str_radix(std::str::from_utf8(rsp).or(Err(Errno::EINVAL))?, 16)
            {
                return Ok(rsp);
            }
        }
    }

    Err(Errno::ENOENT)
}

/// Retrieves the value of /proc/sys/vm/mmap_min_addr.
pub fn proc_mmap_min_addr() -> Result<u64, Errno> {
    let data = read_to_string("/proc/sys/vm/mmap_min_addr").or(Err(Errno::EPERM))?;
    u64::from_str(&data).or(Err(Errno::EINVAL))
}

/// Parses /proc/net/unix and returns the inodes of all UNIX domain sockets.
pub fn proc_unix_get_inodes() -> Result<FxHashSet<u64>, Errno> {
    Ok(procfs::net::unix()
        .or(Err(Errno::EPERM))?
        .into_iter()
        .filter(|entry| {
            // The prefix '@' is for abstract sockets.
            entry
                .path
                .as_ref()
                .map(|p| p.as_os_str().as_bytes().first() != Some(&b'@'))
                .unwrap_or(false)
        })
        .map(|entry| entry.inode)
        .collect())
}

#[cfg(test)]
mod tests {
    use nix::sys::stat::umask;

    use super::*;

    #[test]
    fn test_invalid_pid() {
        let result = proc_umask(Pid::from_raw(i32::MAX));
        assert!(result.is_err(), "{result:?}");
    }

    #[test]
    fn test_parsing_valid_umask_values() {
        // This test sets various umask values and then checks if our function correctly identifies them.
        let umasks = [
            Mode::from_bits_truncate(0o0000),
            Mode::from_bits_truncate(0o0002),
            Mode::from_bits_truncate(0o0022),
            Mode::from_bits_truncate(0o0077),
            Mode::from_bits_truncate(0o0777),
        ];

        for &my_umask in &umasks {
            umask(my_umask);
            let result = proc_umask(Pid::this()).unwrap();
            assert_eq!(result, my_umask, "{result:o} != {my_umask:o}");
        }

        // Resetting the umask to a default value after test
        umask(Mode::from_bits_truncate(0o0022));
    }

    #[test]
    fn test_proc_fd() {
        let this = Pid::this();
        let that = Pid::from_raw(1);

        assert_eq!(
            proc_fd(this, Path::new(&format!("/proc/{this}/cwd"))),
            Ok(Some(nix::libc::AT_FDCWD))
        );
        assert_eq!(
            proc_fd(this, Path::new(&format!("/proc/{this}/exe"))),
            Ok(Some(nix::libc::AT_FDCWD))
        );
        assert_eq!(
            proc_fd(this, Path::new(&format!("/proc/{this}/root"))),
            Ok(Some(nix::libc::AT_FDCWD))
        );
        assert_eq!(
            proc_fd(this, Path::new(&format!("/proc/{that}/fd"))),
            Ok(None)
        );
        assert_eq!(
            proc_fd(this, Path::new(&format!("/proc/{this}/fd"))),
            Ok(None)
        );
        assert_eq!(
            proc_fd(this, Path::new(&format!("/proc/{this}/fd/0"))),
            Ok(Some(0))
        );
        assert_eq!(
            proc_fd(this, Path::new(&format!("/proc/{this}/fd/42"))),
            Ok(Some(42))
        );
        assert_eq!(
            proc_fd(this, Path::new(&format!("/proc/{this}/fd/1984"))),
            Ok(Some(1984))
        );
        assert_eq!(
            proc_fd(this, Path::new(&format!("/proc/{this}/task/{that}/fd/7"))),
            Ok(Some(7))
        );

        assert_eq!(
            proc_fd(this, Path::new(&format!("/proc/{that}/cwd"))),
            Err(Errno::ELOOP)
        );
        assert_eq!(
            proc_fd(this, Path::new(&format!("/proc/{that}/exe"))),
            Err(Errno::ELOOP)
        );
        assert_eq!(
            proc_fd(this, Path::new(&format!("/proc/{that}/root"))),
            Err(Errno::ELOOP)
        );
        assert_eq!(
            proc_fd(this, Path::new(&format!("/proc/{that}/fd/0"))),
            Err(Errno::ELOOP)
        );
        assert_eq!(
            proc_fd(this, Path::new(&format!("/proc/{that}/task/{this}/fd/7"))),
            Err(Errno::ELOOP)
        );
    }
}
