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

use std::os::fd::{AsFd, AsRawFd};

use libseccomp::ScmpNotifResp;
use memchr::arch::all::{is_prefix, is_suffix};
use nix::{errno::Errno, NixPath};

use crate::{
    confine::is_valid_ptr,
    kernel::sandbox_path,
    lookup::{FileType, FsFlags},
    magic::ProcMagic,
    path::{XPathBuf, PATH_MAX},
    proc::proc_tgid,
    req::{SysArg, SysFlags, UNotifyEventRequest},
    sandbox::Capability,
};

const READLINK_MAX: usize = PATH_MAX * 16;

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

    // Return EINVAL for negative size.
    // Cap untrusted size to a maximum.
    let size = match usize::try_from(req.data.args[2]) {
        Ok(0) => return request.fail_syscall(Errno::EINVAL),
        Ok(size) => size.min(READLINK_MAX),
        Err(_) => return request.fail_syscall(Errno::EINVAL),
    };

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

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

    syscall_readlink_handler(request, arg, 1, size)
}

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

    // Return EINVAL for negative size.
    let size = match usize::try_from(req.data.args[3]) {
        Ok(0) => return request.fail_syscall(Errno::EINVAL),
        Ok(size) => size.min(READLINK_MAX),
        Err(_) => return request.fail_syscall(Errno::EINVAL),
    };

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

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

    syscall_readlink_handler(request, arg, 2, size)
}

fn syscall_readlink_handler(
    request: UNotifyEventRequest,
    arg: SysArg,
    buf_idx: usize,
    buf_siz: usize,
) -> ScmpNotifResp {
    syscall_handler!(request, |request: UNotifyEventRequest| {
        let req = request.scmpreq;
        let sandbox = request.get_sandbox();

        // Read the remote path.
        let (path, _, empty_path) = request.read_path(&sandbox, arg)?;

        // Check for access, allow access to fd-only calls.
        if !empty_path && sandbox.enabled(Capability::CAP_STAT) {
            let sysname = if buf_idx == 1 {
                "readlink"
            } else {
                "readlinkat"
            };
            sandbox_path(
                Some(&request),
                &sandbox,
                request.scmpreq.pid(), // Unused when request.is_some()
                path.abs(),
                Capability::CAP_STAT,
                false,
                sysname,
            )?;
        }

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

            // Return EINVAL/ENOENT for non-symlinks.
            if !matches!(file_type, FileType::Lnk | FileType::MagicLnk(_)) {
                return if empty_path {
                    // readlinkat(2) on empty path.
                    Err(Errno::ENOENT)
                } else {
                    Err(Errno::EINVAL)
                };
            }

            // Handle magic symlinks as necessary.
            //
            // FileType::Lnk checks are necessary for fd-only calls.
            let maybe_magic_self = match file_type {
                FileType::MagicLnk(ProcMagic::Pid { pid }) => Some((*pid, None)),
                FileType::Lnk if path.abs().is_proc_self(false) => {
                    Some((request.scmpreq.pid(), None))
                }
                FileType::MagicLnk(ProcMagic::Tid { tgid, pid }) => Some((*pid, Some(*tgid))),
                FileType::Lnk if path.abs().is_proc_self(true) => {
                    let pid = request.scmpreq.pid();
                    let tgid = proc_tgid(pid)?;
                    Some((pid, Some(tgid)))
                }
                _ => None,
            };

            if let Some((pid, maybe_tgid)) = maybe_magic_self {
                let buf = if let Some(tgid) = maybe_tgid {
                    XPathBuf::from_task(tgid, pid)
                } else {
                    XPathBuf::from_pid(pid)
                }?;

                let buf = buf.as_bytes();
                let siz = buf.len().min(buf_siz);
                let siz = request.write_mem(&buf[..siz], req.data.args[buf_idx])?;
                #[expect(clippy::cast_possible_wrap)]
                return Ok(request.return_syscall(siz as i64));
            }
        }

        // 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)?;

        // Allocate buffer.
        // Size is already capped to a safe maximum.
        let mut buf = Vec::new();
        buf.try_reserve(buf_siz).or(Err(Errno::ENOMEM))?;
        buf.resize(buf_siz, 0);

        // Make the readlinkat(2) syscall.
        //
        // SAFETY:
        // 1. We use fd-only with empty path to avoid TOCTTOU.
        // 2. In libc we trust. nix' wrapper is unusable here.
        #[expect(clippy::cast_sign_loss)]
        let size = Errno::result(unsafe {
            libc::readlinkat(
                fd.as_raw_fd(),
                c"".as_ptr(),
                buf.as_mut_ptr().cast(),
                buf_siz,
            )
        })
        .map(|size| size as usize)?;

        // Rearrange !memfd:syd/ links.
        let mut buf = &buf[..size];
        if is_prefix(buf, b"/memfd:syd") {
            buf = &buf[b"/memfd:syd".len()..];
            if is_suffix(buf, b" (deleted)") {
                buf = &buf[..buf.len().saturating_sub(b" (deleted)".len())];
            }
        }

        // readlink(2) truncates and does NOT add a NUL-byte.
        let size = request.write_mem(buf, req.data.args[buf_idx])?;

        // readlink(2) system call has been successfully emulated.
        #[expect(clippy::cast_possible_wrap)]
        Ok(request.return_syscall(size as i64))
    })
}
