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

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

use libseccomp::ScmpNotifResp;
use nix::{
    errno::Errno,
    fcntl::AtFlags,
    sys::stat::{fchmod, Mode},
    NixPath,
};

use crate::{
    config::PROC_FILE,
    cookie::{safe_fchmod, safe_fchmodat},
    kernel::{syscall_path_handler, to_atflags, to_mode},
    lookup::FsFlags,
    path::XPathBuf,
    req::{PathArgs, SysArg, UNotifyEventRequest},
    sandbox::SandboxGuard,
};

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

    // SAFETY: Strip undefined/invalid mode bits.
    let mode = to_mode(req.data.args[1]);

    // WANT_READ: fchmod(2) does not work with O_PATH fds.
    let argv = &[SysArg {
        dirfd: Some(0),
        fsflags: FsFlags::MUST_PATH | FsFlags::WANT_READ,
        ..Default::default()
    }];
    syscall_path_handler(request, "fchmod", argv, |path_args, request, sandbox| {
        // SAFETY: SysArg has one element.
        #[expect(clippy::disallowed_methods)]
        let path = path_args.0.as_ref().unwrap();

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

        // SAFETY:
        // We apply force_umask to chmod modes to ensure consistency.
        // Umask is only forced for regular files.
        let mut mode = mode;
        if path.typ.map(|typ| typ.is_file()).unwrap_or(false) {
            let umask = sandbox.umask.unwrap_or(Mode::empty());
            mode &= !umask;
        }
        drop(sandbox); // release the read-lock.

        fchmod(fd, mode).map(|_| request.return_syscall(0))
    })
}

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

    // SAFETY: Strip undefined/invalid mode bits.
    let mode = to_mode(req.data.args[1]);

    let argv = &[SysArg {
        path: Some(0),
        ..Default::default()
    }];

    syscall_path_handler(request, "chmod", argv, |path_args, request, sandbox| {
        syscall_chmod_handler(request, sandbox, path_args, mode)
    })
}

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

    // SAFETY: Strip undefined/invalid mode bits.
    let mode = to_mode(req.data.args[2]);

    // Note: Unlike fchmodat2, fchmodat always resolves symbolic links.
    let argv = &[SysArg {
        dirfd: Some(0),
        path: Some(1),
        ..Default::default()
    }];

    syscall_path_handler(request, "fchmodat", argv, |path_args, request, sandbox| {
        syscall_chmod_handler(request, sandbox, path_args, mode)
    })
}

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

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

    // SAFETY: Strip undefined/invalid mode bits.
    let mode = to_mode(req.data.args[2]);

    let mut fsflags = FsFlags::MUST_PATH;
    if flags.contains(AtFlags::AT_SYMLINK_NOFOLLOW) {
        fsflags |= FsFlags::NO_FOLLOW_LAST
    }

    let argv = &[SysArg {
        dirfd: Some(0),
        path: Some(1),
        fsflags,
        ..Default::default()
    }];

    syscall_path_handler(request, "fchmodat2", argv, |path_args, request, sandbox| {
        syscall_chmod_handler(request, sandbox, path_args, mode)
    })
}

/// A helper function to handle chmod, fchmodat, and fchmodat2 syscalls.
fn syscall_chmod_handler(
    request: &UNotifyEventRequest,
    sandbox: SandboxGuard,
    args: PathArgs,
    mut mode: Mode,
) -> Result<ScmpNotifResp, Errno> {
    // SAFETY: SysArg has one element.
    #[expect(clippy::disallowed_methods)]
    let path = args.0.as_ref().unwrap();

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

    // SAFETY:
    // We apply force_umask to chmod modes to ensure consistency.
    // Umask is only forced for regular files.
    if path.typ.map(|typ| typ.is_file()).unwrap_or(false) {
        let umask = sandbox.umask.unwrap_or(Mode::empty());
        mode &= !umask;
    }
    drop(sandbox); // release the read-lock.

    match safe_fchmod(fd, mode) {
        Ok(_) => Ok(()),
        Err(Errno::ENOSYS) => {
            // Fallback to `/proc` indirection,
            //
            // path to fd is open already!
            let pfd = XPathBuf::from_self_fd(fd.as_raw_fd())?;
            safe_fchmodat(PROC_FILE(), &pfd, mode)
        }
        Err(errno) => Err(errno),
    }
    .map(|_| request.return_syscall(0))
}
