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

use std::{ops::Deref, os::fd::RawFd};

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

use crate::{
    compat::getdents64, config::DIRENT_BUF_SIZE, confine::is_valid_ptr, kernel::sandbox_path,
    lookup::CanonicalPath, req::UNotifyEventRequest, sandbox::Capability,
};

pub(crate) fn sys_getdents64(request: UNotifyEventRequest) -> ScmpNotifResp {
    syscall_handler!(request, |request: UNotifyEventRequest| {
        let req = request.scmpreq;

        // Validate result buffer.
        if req.data.args[2] == 0 {
            return Err(Errno::EINVAL);
        }

        // Validate file descriptor.
        //
        // AT_FDCWD is an invalid file descriptor.
        let fd = RawFd::try_from(req.data.args[0]).or(Err(Errno::EBADF))?;
        if fd < 0 {
            return Err(Errno::EBADF);
        }

        // Validate dirp pointer.
        if !is_valid_ptr(req.data.args[1], req.data.arch) {
            // SAFETY: If the second argument which must hold a pointer to a
            // linux_dirent structure is not valid, we must return EFAULT
            // without further processing here.
            return Err(Errno::EFAULT);
        }

        // Get remote fd, and
        // Readlink /proc/thread-self/fd/$fd.
        //
        // Note, the Readdir access check here has been
        // moved to the _open_(2) handler for simplicity and
        // efficiency. The Stat check still takes place.
        let fd = request.get_fd(fd)?;
        let mut path = CanonicalPath::new_fd(fd.into(), req.pid())?;
        if !path.is_dir() {
            return Err(Errno::ENOTDIR);
        }
        #[expect(clippy::disallowed_methods)]
        let fd = path.dir.take().unwrap();
        let mut dir = path.take();

        // SAFETY: The count argument to the getdents call
        // must not be fully trusted, it can be overly large,
        // and allocating a Vector of that capacity may overflow.
        // This bug was discovered by trinity in this build:
        // https://builds.sr.ht/~alip/job/1077263
        let count = usize::try_from(req.data.args[2])
            .or(Err(Errno::EINVAL))?
            .min(DIRENT_BUF_SIZE);
        if count == 0 {
            // Reject zero size _after_ ENOTDIR check.
            return Err(Errno::EINVAL);
        }
        // There's no guarantee on the order of items returned by getdents64(2).
        // Therefore we must potentially check each element for dot or dotdot,
        // until we actually see one.
        let mut seen_dot = false;
        let mut seen_dotdot = false;
        let pid = req.pid();
        let len = dir.len();
        let mut ret: usize = 0;
        while ret == 0 {
            let siz = count.checked_sub(ret).ok_or(Errno::EOVERFLOW)?;
            if siz == 0 {
                break;
            }
            let mut entries = match getdents64(&fd, siz) {
                Ok(entries) => entries,
                Err(Errno::ECANCELED) => break, // EOF or empty directory
                Err(errno) => return Err(errno),
            };

            // Lock sandbox for read to perform Stat access check.
            let sandbox = request.get_sandbox();
            let check_flags = sandbox.flags.deref().into();

            for entry in &mut entries {
                // SAFETY: Allow the special dot entries `.` and `..`.
                // Note, `..` may point to a denylisted directory,
                // however at this point there's not much we can do:
                // even the root directory, ie `/`, has a `..`. In
                // this exceptional case `..` points to `.`.
                if !seen_dot && entry.is_dot() {
                    seen_dot = true;
                } else if !seen_dotdot && entry.is_dotdot() {
                    seen_dotdot = true;
                } else {
                    // Append entry name to the directory.
                    dir.push(entry.name_bytes());

                    // SAFETY: Run XPath::check() with file type for global restrictions.
                    if dir
                        .check(
                            pid,
                            Some(&entry.file_type()),
                            Some(entry.as_xpath()),
                            check_flags,
                        )
                        .is_err()
                    {
                        // Skip entry.
                        dir.truncate(len);
                        continue;
                    }

                    // SAFETY: Run sandbox access check with Stat capability.
                    let hide = sandbox_path(
                        Some(&request),
                        &sandbox,
                        request.scmpreq.pid(), // Unused when request.is_some()
                        &dir,
                        Capability::CAP_STAT,
                        true,
                        "getdents64",
                    )
                    .is_err();

                    // Restore directory entry.
                    dir.truncate(len);

                    if hide {
                        // Skip entry.
                        continue;
                    }
                }

                // Access granted, write entry to sandbox process memory.
                // Handle truncation as necessary.
                let buf = entry.as_bytes();
                let siz = count.checked_sub(ret).ok_or(Errno::EOVERFLOW)?;
                let siz = buf.len().min(siz);
                let ptr = req.data.args[1]
                    .checked_add(ret as u64)
                    .ok_or(Errno::EOVERFLOW)?;
                match request.write_mem(&buf[..siz], ptr) {
                    Ok(n) => {
                        ret = ret.checked_add(n).ok_or(Errno::EOVERFLOW)?;
                        if n != entry.size() || ret >= count {
                            break;
                        }
                    }
                    Err(_) if ret > 0 => break,
                    Err(errno) => return Err(errno),
                };
            }
        }

        #[expect(clippy::cast_possible_wrap)]
        Ok(request.return_syscall(ret as i64))
    })
}
