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

use std::{fs::File, io::BufReader, os::fd::AsFd};

use libseccomp::ScmpNotifResp;
use nix::{errno::Errno, fcntl::AtFlags, NixPath};

use crate::{
    compat::{
        fstatat64, statx, FileStat64, FileStatx, FileStatxTimestamp, STATX_BASIC_STATS, STATX_MODE,
        STATX_TYPE,
    },
    config::{API_VERSION, MAGIC_PREFIX},
    confine::{is_valid_ptr, scmp_arch_bits, EOWNERDEAD},
    fd::parse_fd,
    hash::SydHashSet,
    kernel::{sandbox_path, to_atflags},
    lookup::{CanonicalPath, FileInfo, FileType, FsFlags},
    path::XPath,
    req::{SysArg, SysFlags, UNotifyEventRequest},
    sandbox::{Capability, Flags},
};

pub(crate) fn sys_stat(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // Return EFAULT here for invalid pointers.
    if !is_valid_ptr(req.data.args[1], req.data.arch) {
        return request.fail_syscall(Errno::EFAULT);
    }

    let is32 = scmp_arch_bits(req.data.arch) == 32;

    let arg = SysArg {
        path: Some(0),
        flags: SysFlags::CHECK_MAGIC,
        fsflags: FsFlags::MUST_PATH,
        ..Default::default()
    };

    syscall_stat_handler(request, arg, 1, is32)
}

pub(crate) fn sys_stat64(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // Return EFAULT here for invalid pointers.
    if !is_valid_ptr(req.data.args[1], req.data.arch) {
        return request.fail_syscall(Errno::EFAULT);
    }

    let arg = SysArg {
        path: Some(0),
        flags: SysFlags::CHECK_MAGIC,
        fsflags: FsFlags::MUST_PATH,
        ..Default::default()
    };

    syscall_stat_handler(request, arg, 1, false)
}

pub(crate) fn sys_fstat(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // Return EFAULT here for invalid pointers.
    if !is_valid_ptr(req.data.args[1], req.data.arch) {
        return request.fail_syscall(Errno::EFAULT);
    }

    let is32 = scmp_arch_bits(req.data.arch) == 32;

    let arg = SysArg {
        dirfd: Some(0),
        ..Default::default()
    };

    syscall_stat_handler(request, arg, 1, is32)
}

pub(crate) fn sys_fstat64(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // Return EFAULT here for invalid pointers.
    if !is_valid_ptr(req.data.args[1], req.data.arch) {
        return request.fail_syscall(Errno::EFAULT);
    }

    let arg = SysArg {
        dirfd: Some(0),
        ..Default::default()
    };

    syscall_stat_handler(request, arg, 1, false)
}

pub(crate) fn sys_lstat(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // Return EFAULT here for invalid pointers.
    if !is_valid_ptr(req.data.args[1], req.data.arch) {
        return request.fail_syscall(Errno::EFAULT);
    }

    let is32 = scmp_arch_bits(req.data.arch) == 32;

    let arg = SysArg {
        path: Some(0),
        flags: SysFlags::CHECK_MAGIC,
        fsflags: FsFlags::MUST_PATH | FsFlags::NO_FOLLOW_LAST,
        ..Default::default()
    };

    syscall_stat_handler(request, arg, 1, is32)
}

pub(crate) fn sys_lstat64(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // Return EFAULT here for invalid pointers.
    if !is_valid_ptr(req.data.args[1], req.data.arch) {
        return request.fail_syscall(Errno::EFAULT);
    }

    let arg = SysArg {
        path: Some(0),
        flags: SysFlags::CHECK_MAGIC,
        fsflags: FsFlags::MUST_PATH | FsFlags::NO_FOLLOW_LAST,
        ..Default::default()
    };

    syscall_stat_handler(request, arg, 1, false)
}

pub(crate) fn sys_statx(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // SAFETY: Reject undefined/invalid flags.
    // const AT_STATX_SYNC_AS_STAT: AtFlags = AtFlags::empty();
    const AT_STATX_FORCE_SYNC: AtFlags = AtFlags::from_bits_retain(0x2000);
    const AT_STATX_DONT_SYNC: AtFlags = AtFlags::from_bits_retain(0x4000);
    let atflags = match to_atflags(
        req.data.args[2],
        AtFlags::AT_EMPTY_PATH
            | AtFlags::AT_SYMLINK_NOFOLLOW
            | AtFlags::AT_NO_AUTOMOUNT
            | AT_STATX_FORCE_SYNC
            | AT_STATX_DONT_SYNC,
    ) {
        Ok(atflags) => atflags,
        Err(errno) => return request.fail_syscall(errno),
    };

    // Return EFAULT here for invalid pointers.
    if !is_valid_ptr(req.data.args[4], req.data.arch) {
        return request.fail_syscall(Errno::EFAULT);
    }

    let mut flags = SysFlags::empty();
    let mut fsflags = FsFlags::MUST_PATH;
    if atflags.contains(AtFlags::AT_EMPTY_PATH) {
        flags |= SysFlags::EMPTY_PATH;
    } else {
        flags |= SysFlags::CHECK_MAGIC;
    }
    if atflags.contains(AtFlags::AT_SYMLINK_NOFOLLOW) {
        fsflags |= FsFlags::NO_FOLLOW_LAST;
    }

    let arg = SysArg {
        dirfd: Some(0),
        path: Some(1),
        flags,
        fsflags,
        ..Default::default()
    };

    syscall_stat_handler(request, arg, 4, false)
}

pub(crate) fn sys_newfstatat(request: UNotifyEventRequest) -> ScmpNotifResp {
    let req = request.scmpreq;

    // Return EFAULT here for invalid pointers.
    if !is_valid_ptr(req.data.args[2], req.data.arch) {
        return request.fail_syscall(Errno::EFAULT);
    }

    // SAFETY: Reject undefined/invalid flags.
    let atflags = match to_atflags(
        req.data.args[3],
        AtFlags::AT_EMPTY_PATH | AtFlags::AT_SYMLINK_NOFOLLOW,
    ) {
        Ok(atflags) => atflags,
        Err(errno) => return request.fail_syscall(errno),
    };

    let mut flags = SysFlags::empty();
    let mut fsflags = FsFlags::MUST_PATH;
    if atflags.contains(AtFlags::AT_EMPTY_PATH) {
        flags |= SysFlags::EMPTY_PATH;
    } else {
        flags |= SysFlags::CHECK_MAGIC;
    }
    if atflags.contains(AtFlags::AT_SYMLINK_NOFOLLOW) {
        fsflags |= FsFlags::NO_FOLLOW_LAST;
    }

    let arg = SysArg {
        dirfd: Some(0),
        path: Some(1),
        flags,
        fsflags,
        ..Default::default()
    };

    syscall_stat_handler(request, arg, 2, false)
}

#[expect(clippy::cognitive_complexity)]
fn syscall_stat_handler(
    request: UNotifyEventRequest,
    arg: SysArg,
    arg_stat: usize,
    is32: bool,
) -> ScmpNotifResp {
    syscall_handler!(request, |request: UNotifyEventRequest| {
        let req = request.scmpreq;
        let sandbox = request.get_sandbox();

        let has_crypt = sandbox.enabled(Capability::CAP_CRYPT);
        let has_stat = sandbox.enabled(Capability::CAP_STAT);

        // Read the remote path and check for magic path as necessary.
        let (mut path, magic, empty_path) = request.read_path(&sandbox, arg)?;
        let is_fd = empty_path || arg.path.is_none();

        // Check for chroot.
        if sandbox.is_chroot() {
            return Err(if is_fd { Errno::EACCES } else { Errno::ENOENT });
        }

        let restrict_stat_bdev = !sandbox.flags.allow_unsafe_stat_bdev();
        let restrict_stat_cdev = !sandbox.flags.allow_unsafe_stat_cdev();
        let mut ghost = false;
        let caps = *sandbox.state;
        let sflags = *sandbox.flags;
        if magic {
            if sandbox.locked_for(req.pid()) {
                // Sandbox is locked, access denied.
                return Err(Errno::ENOENT);
            }
            drop(sandbox); // release the read-lock.

            // Handle magic prefix (ie /dev/syd)
            let mut cmd = path
                .abs()
                .strip_prefix(MAGIC_PREFIX)
                .unwrap_or_else(|| XPath::from_bytes(&path.abs().as_bytes()[MAGIC_PREFIX.len()..]))
                .to_owned();
            // Careful here, Path::strip_prefix removes trailing slashes.
            if path.abs().ends_with_slash() {
                cmd.push(b"");
            }

            // Acquire a write lock to the sandbox.
            let mut sandbox = request.get_mut_sandbox();

            // Execute magic command.
            if cmd.is_empty() || cmd.is_equal(b".el") || cmd.is_equal(b".sh") {
                sandbox.config("")?;
            } else if cmd.is_equal(b"panic") {
                sandbox.panic()?;
            } else if cmd.is_equal(b"ghost") {
                // SAFETY: Reset sandbox to ensure no run-away execs.
                sandbox.reset()?;
                ghost = true;
            } else if let Some(cmd) = cmd.strip_prefix(b"load") {
                // We handle load specially here as it involves process access.
                // 1. Attempt to parse as FD, pidfd_getfd and load it.
                // 2. Attempt to parse as profile name if (1) fails.
                match parse_fd(cmd) {
                    Ok(remote_fd) => {
                        let fd = request.get_fd(remote_fd)?;
                        let file = BufReader::new(File::from(fd));
                        let mut imap = SydHashSet::default();
                        // SAFETY: parse_config() checks for the file name
                        // /dev/syd/load and disables config file include
                        // feature depending on this check.
                        if sandbox
                            .parse_config(file, XPath::from_bytes(b"/dev/syd/load"), &mut imap)
                            .is_err()
                        {
                            return Ok(request.fail_syscall(Errno::EINVAL));
                        }
                        // Fall through to emulate as /dev/null.
                    }
                    Err(Errno::EBADF) => {
                        if sandbox.parse_profile(&cmd.to_string()).is_err() {
                            return Ok(request.fail_syscall(Errno::EINVAL));
                        }
                        // Fall through to emulate as /dev/null.
                    }
                    Err(errno) => {
                        return Ok(request.fail_syscall(errno));
                    }
                }
            } else if let Ok(cmd) = std::str::from_utf8(cmd.as_bytes()) {
                sandbox.config(cmd)?;
            } else {
                // SAFETY: Invalid UTF-8 is not permitted.
                // To include non-UTF-8, hex-encode them.
                return Err(Errno::EINVAL);
            }
            drop(sandbox); // release the write-lock.
        } else {
            // Handle fstat for files with encryption in progress.
            #[expect(clippy::disallowed_methods)]
            if is_fd && has_crypt {
                // SAFETY: SysArg.path is None asserting dirfd is Some fd!=AT_FDCWD.
                let fd = path.dir.as_ref().unwrap();
                // SAFETY: has_crypt asserts crypt_map is Some.
                let files = request.cache.crypt_map.as_ref().unwrap();

                if let Ok(info) = FileInfo::from_fd(fd) {
                    let files = files.0.lock().unwrap_or_else(|err| err.into_inner());
                    for (enc_path, map) in files.iter() {
                        if info == map.info {
                            // Found underlying encrypted file for the memory fd.
                            // Note, we only ever attempt to encrypt regular files.
                            path = CanonicalPath::new_crypt(
                                path.dir.take().unwrap(),
                                enc_path.clone(),
                            );
                            break;
                        }
                    }
                } // Lock is released here.
            }

            // Return correct stat information for !memfd:syd/ paths.
            // This prefix is internal to Syd and sandbox process cannot
            // create memory file descriptors with this name prefix.
            if is_fd && path.is_memory_fd() && path.abs().starts_with(b"!memfd:syd") {
                let mut p = path.take();
                p.drain(0..b"!memfd:syd".len());
                path = CanonicalPath::new_mask(&p, &p)?;
            }

            // SAFETY:
            // 1. Allow access to fd-only calls.
            // 2. Allow access to files with encryption in progress.
            if has_stat && arg.path.is_some() {
                sandbox_path(
                    Some(&request),
                    &sandbox,
                    request.scmpreq.pid(), // Unused when request.is_some()
                    path.abs(),
                    Capability::CAP_STAT,
                    false,
                    "stat",
                )?;
            }

            drop(sandbox); // release the read-lock.
        }

        // SAFETY: Path hiding is done, now it is safe to:
        //
        // Return ENOTDIR for non-directories with trailing slash.
        if let Some(file_type) = &path.typ {
            if !matches!(file_type, FileType::Dir | FileType::MagicLnk(_))
                && path.abs().last() == Some(b'/')
            {
                return Err(Errno::ENOTDIR);
            }
        }

        // We use MUST_PATH, dir refers to the file.
        assert!(
            path.base().is_empty(),
            "BUG: MUST_PATH returned a directory for stat, report a bug!"
        );
        let fd = path.dir.as_ref().map(|fd| fd.as_fd()).ok_or(Errno::EBADF)?;
        let mut flags = libc::AT_EMPTY_PATH;

        #[expect(clippy::cast_possible_truncation)]
        if arg_stat == 4 {
            // statx

            // Support AT_STATX_* flags.
            flags |= req.data.args[2] as libc::c_int
                & !(libc::AT_SYMLINK_NOFOLLOW | libc::AT_EMPTY_PATH);

            // SAFETY: The sidechannel check below requires the mask
            // to have the following items:
            // 1. STATX_TYPE (to check for char/block device)
            // 2. STATX_MODE (to check for world readable/writable)
            // To ensure that here, we inject these two flags into
            // mask noting if they were set originally. This can be
            // in three ways,
            // (a) Explicitly setting STATX_{TYPE,MODE}.
            // (b) Explicitly setting STATX_BASIC_STATS.
            // (c) Setting the catch-all STATX_ALL flag.
            // After the statx call if the flags STATX_{TYPE,MODE}
            // were not set we clear stx_mode's type and mode bits
            // as necessary and also remove STATX_{TYPE,MODE} from
            // stx_mask as necessary.
            let mut mask = req.data.args[3] as libc::c_uint;
            let orig_mask = mask;
            let basic_stx = (orig_mask & STATX_BASIC_STATS) != 0;
            if !basic_stx {
                mask |= STATX_TYPE | STATX_MODE;
            }

            // Note, unlike statfs, stat does not EINTR.
            let mut statx = statx(fd, c"", flags, mask)?;

            // SAFETY: Check if the file is a sidechannel device and
            // update its access and modification times to match the
            // creation time if it is. This prevents timing attacks on
            // block or character devices like /dev/ptmx using stat.
            if restrict_stat_bdev || restrict_stat_cdev {
                let filetype = FileType::from(libc::mode_t::from(statx.stx_mode));
                if (restrict_stat_bdev && filetype.is_block_device())
                    || (restrict_stat_cdev && filetype.is_char_device())
                {
                    statx.stx_atime = statx.stx_ctime;
                    statx.stx_mtime = statx.stx_ctime;
                }
            }

            // SAFETY: Restore mask, type and mode, see the comment above.
            #[expect(clippy::cast_possible_truncation)]
            if !basic_stx {
                if (orig_mask & STATX_TYPE) == 0 {
                    statx.stx_mode &= !libc::S_IFMT as u16;
                    statx.stx_mask &= !STATX_TYPE;
                }
                if (orig_mask & STATX_MODE) == 0 {
                    statx.stx_mode &= libc::S_IFMT as u16;
                    statx.stx_mask &= !STATX_MODE;
                }
            }

            // If magic path, mask values for easy id.
            if magic {
                magic_statx(&mut statx, caps, sflags);
            }

            // SAFETY: The following block creates an immutable byte
            // slice representing the memory of `statx`. We ensure that
            // the slice covers the entire memory of `statx` using
            // `size_of_val`. Since `statx` is a stack
            // variable and we're only borrowing its memory for the
            // duration of the slice, there's no risk of `statx` being
            // deallocated while the slice exists. Additionally, we
            // ensure that the slice is not used outside of its valid
            // lifetime.
            let statx = unsafe {
                std::slice::from_raw_parts(
                    std::ptr::addr_of!(statx) as *const u8,
                    size_of_val(&statx),
                )
            };
            let addr = req.data.args[4];
            if addr != 0 {
                request.write_mem(statx, addr)?;
            }
        } else {
            // "stat" | "fstat" | "lstat" | "newfstatat"

            // SAFETY: In libc we trust.
            // Note, unlike statfs, stat does not EINTR.
            let mut stat = fstatat64(fd, c"", flags)?;

            // SAFETY: Check if the file is a sidechannel device and
            // update its access and modification times to match the
            // creation time if it is. This prevents timing attacks on
            // block or character devices like /dev/ptmx using stat.
            if restrict_stat_bdev || restrict_stat_cdev {
                let filetype = FileType::from(stat.st_mode);
                if (restrict_stat_bdev && filetype.is_block_device())
                    || (restrict_stat_cdev && filetype.is_char_device())
                {
                    stat.st_atime = stat.st_ctime;
                    stat.st_mtime = stat.st_ctime;
                    stat.st_atime_nsec = stat.st_ctime_nsec;
                    stat.st_mtime_nsec = stat.st_ctime_nsec;
                }
            }

            // If magic path, mask values for easy id.
            if magic {
                magic_stat(&mut stat, caps, sflags);
            }

            let addr = req.data.args[arg_stat];
            if addr != 0 {
                if is32 {
                    let stat32: crate::compat::stat32 = stat.into();

                    // SAFETY: The following block creates an immutable
                    // byte slice representing the memory of `stat`.  We
                    // ensure that the slice covers the entire memory of
                    // `stat` using `size_of_val`. Since
                    // `stat` is a stack variable and we're only
                    // borrowing its memory for the duration of the
                    // slice, there's no risk of `stat` being
                    // deallocated while the slice exists.
                    // Additionally, we ensure that the slice is not
                    // used outside of its valid lifetime.
                    let stat = unsafe {
                        std::slice::from_raw_parts(
                            std::ptr::addr_of!(stat32) as *const u8,
                            size_of_val(&stat32),
                        )
                    };
                    request.write_mem(stat, addr)?;
                } else {
                    // SAFETY: The following block creates an immutable
                    // byte slice representing the memory of `stat`.  We
                    // ensure that the slice covers the entire memory of
                    // `stat` using `size_of_val`. Since
                    // `stat` is a stack variable and we're only
                    // borrowing its memory for the duration of the
                    // slice, there's no risk of `stat` being
                    // deallocated while the slice exists.
                    // Additionally, we ensure that the slice is not
                    // used outside of its valid lifetime.
                    let stat = unsafe {
                        std::slice::from_raw_parts(
                            std::ptr::addr_of!(stat) as *const u8,
                            size_of_val(&stat),
                        )
                    };
                    request.write_mem(stat, addr)?;
                }
            }
        }

        // Use the pseudo errno(3) EOWNERDEAD to initiate ghost mode.
        // We only do it here to ensure metadata of /dev/null was
        // written to sandbox process memory.
        if ghost {
            return Ok(ScmpNotifResp::new(0, 0, EOWNERDEAD, 0));
        }

        // stat(2) system call has been successfully emulated.
        Ok(request.return_syscall(0))
    })
}

fn magic_stat(stat: &mut FileStat64, caps: Capability, flags: Flags) {
    stat.st_ino = 0;
    stat.st_nlink = caps.nlink().into();
    stat.st_mode = magic_mode(caps, flags).into();
    stat.st_rdev = API_VERSION.dev();
    stat.st_atime = 505958400; // must match libsyd!
    stat.st_ctime = -2036448000; // ditto!
    stat.st_mtime = -842745600; // ditto!
}

fn magic_statx(statx: &mut FileStatx, caps: Capability, flags: Flags) {
    statx.stx_ino = 0;
    statx.stx_nlink = caps.nlink();
    statx.stx_mode = magic_mode(caps, flags);
    statx.stx_rdev_major = API_VERSION.major().into();
    statx.stx_rdev_minor = API_VERSION.minor().into();
    statx.stx_atime = FileStatxTimestamp {
        tv_sec: 505958400, // must match libsyd!
        ..Default::default()
    };
    statx.stx_ctime = FileStatxTimestamp {
        tv_sec: -2036448000, // ditto!
        ..Default::default()
    };
    statx.stx_mtime = FileStatxTimestamp {
        tv_sec: -842745600, // ditto!
        ..Default::default()
    };
}

#[allow(clippy::cognitive_complexity)]
#[allow(clippy::cast_possible_truncation)]
fn magic_mode(caps: Capability, flags: Flags) -> u16 {
    // Start with file type = character device
    let mut mode: u16 = libc::S_IFCHR as u16;

    // Special bits
    if flags.contains(Flags::FL_UNSHARE_MOUNT) {
        mode |= libc::S_ISVTX as u16;
    }
    if flags.contains(Flags::FL_UNSHARE_USER) {
        mode |= libc::S_ISUID as u16;
    }
    if flags.contains(Flags::FL_UNSHARE_NET) {
        mode |= libc::S_ISGID as u16;
    }

    // Permission bits for owner/group/other
    if caps.contains(Capability::CAP_READ) {
        mode |= libc::S_IRUSR as u16;
    }
    if caps.contains(Capability::CAP_WRITE) {
        mode |= libc::S_IWUSR as u16;
    }
    if caps.contains(Capability::CAP_EXEC) {
        mode |= libc::S_IXUSR as u16;
    }
    if caps.contains(Capability::CAP_STAT) {
        mode |= libc::S_IRGRP as u16;
    }
    if caps.contains(Capability::CAP_PROXY) {
        mode |= libc::S_IWGRP as u16;
    }
    if caps.contains(Capability::CAP_TPE) {
        mode |= libc::S_IXGRP as u16;
    }
    if caps.contains(Capability::CAP_LOCK) {
        mode |= libc::S_IROTH as u16;
    }
    if caps.contains(Capability::CAP_CRYPT) {
        mode |= libc::S_IWOTH as u16;
    }
    if caps.contains(Capability::CAP_FORCE) {
        mode |= libc::S_IXOTH as u16;
    }

    mode
}
