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

use std::os::fd::AsFd;

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

use crate::{
    cookie::safe_unlinkat,
    kernel::syscall_path_handler,
    lookup::FsFlags,
    req::{PathArgs, SysArg, UNotifyEventRequest},
};

pub(crate) fn sys_rmdir(request: UNotifyEventRequest) -> ScmpNotifResp {
    // rmdir() does not work on fds!
    // Hence, we have to use WANT_BASE to split base.
    let argv = &[SysArg {
        path: Some(0),
        dotlast: Some(Errno::EINVAL),
        fsflags: FsFlags::NO_FOLLOW_LAST | FsFlags::MUST_PATH | FsFlags::WANT_BASE,
        ..Default::default()
    }];
    syscall_path_handler(request, "rmdir", argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.

        syscall_unlink_handler(request, path_args, true)
    })
}

pub(crate) fn sys_unlink(request: UNotifyEventRequest) -> ScmpNotifResp {
    // unlink() does not work on fds!
    // Hence, we have to use WANT_BASE to split base.
    let argv = &[SysArg {
        path: Some(0),
        dotlast: Some(Errno::EINVAL),
        fsflags: FsFlags::NO_FOLLOW_LAST | FsFlags::MUST_PATH | FsFlags::WANT_BASE,
        ..Default::default()
    }];
    syscall_path_handler(request, "unlink", argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.

        syscall_unlink_handler(request, path_args, false)
    })
}

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

    // SAFETY: Reject undefined/invalid flags.
    let flags: libc::c_int = match req.data.args[2].try_into() {
        Ok(flags) if flags & !libc::AT_REMOVEDIR != 0 => {
            return request.fail_syscall(Errno::EINVAL)
        }
        Ok(flags) => flags,
        Err(_) => return request.fail_syscall(Errno::EINVAL),
    };

    // unlinkat() does not work on fds!
    // Hence, we have to use WANT_BASE to split base.
    let rmdir = flags & libc::AT_REMOVEDIR != 0;
    let argv = &[SysArg {
        dirfd: Some(0),
        path: Some(1),
        dotlast: Some(Errno::EINVAL),
        fsflags: FsFlags::NO_FOLLOW_LAST | FsFlags::MUST_PATH | FsFlags::WANT_BASE,
        ..Default::default()
    }];
    syscall_path_handler(request, "unlinkat", argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.

        syscall_unlink_handler(request, path_args, rmdir)
    })
}

/// A helper function to handle rmdir and unlink{,at} syscalls.
fn syscall_unlink_handler(
    request: &UNotifyEventRequest,
    args: PathArgs,
    rmdir: bool,
) -> Result<ScmpNotifResp, Errno> {
    // SAFETY: SysArg has one element.
    #[expect(clippy::disallowed_methods)]
    let path = args.0.as_ref().unwrap();
    let dir = path.dir.as_ref().map(|fd| fd.as_fd()).ok_or(Errno::EBADF)?;

    // SAFETY: Path hiding is done, not it is safe to:
    // Return E{IS,NOT}DIR if path is not an expected dir/non-dir.
    // Return EBUSY if path is the root directory.
    // `path.typ' may be None if permission was denied to stat etc.
    if let Some(ftyp) = path.typ {
        if ftyp.is_dir() && !rmdir {
            return Err(Errno::EISDIR);
        }
        if !ftyp.is_dir() && rmdir {
            return Err(Errno::ENOTDIR);
        }
    }
    if path.base().is_empty() {
        return Err(Errno::EBUSY);
    }

    let flags = if rmdir {
        UnlinkatFlags::RemoveDir
    } else {
        UnlinkatFlags::NoRemoveDir
    };
    safe_unlinkat(dir, path.base(), flags).map(|_| request.return_syscall(0))
}
