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

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

use libseccomp::ScmpNotifResp;
use nix::{
    errno::Errno,
    fcntl::AtFlags,
    unistd::{fchown, Gid, Uid},
    NixPath,
};

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

pub(crate) fn sys_fchown(request: UNotifyEventRequest) -> ScmpNotifResp {
    // WANT_READ: fchown(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, "fchown", argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.

        // SAFETY:
        // 1. SysArg has one element.
        // 2. SysArg.path is None asserting dir is Some.
        #[expect(clippy::disallowed_methods)]
        let fd = path_args.0.as_ref().unwrap().dir.as_ref().unwrap();

        let req = request.scmpreq;
        let owner = libc::uid_t::try_from(req.data.args[1])
            .map(Uid::from_raw)
            .ok();
        let group = libc::gid_t::try_from(req.data.args[2])
            .map(Gid::from_raw)
            .ok();
        if owner.is_none() && group.is_none() {
            // Nothing to change.
            return Ok(request.return_syscall(0));
        }

        fchown(fd, owner, group).map(|_| request.return_syscall(0))
    })
}

pub(crate) fn sys_chown(request: UNotifyEventRequest) -> ScmpNotifResp {
    let argv = &[SysArg {
        path: Some(0),
        ..Default::default()
    }];
    syscall_path_handler(request, "chown", argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.

        let req = request.scmpreq;
        let owner = libc::uid_t::try_from(req.data.args[1])
            .map(Uid::from_raw)
            .ok();
        let group = libc::gid_t::try_from(req.data.args[2])
            .map(Gid::from_raw)
            .ok();
        syscall_chown_handler(request, path_args, owner, group)
    })
}

pub(crate) fn sys_lchown(request: UNotifyEventRequest) -> ScmpNotifResp {
    let argv = &[SysArg {
        path: Some(0),
        fsflags: FsFlags::MUST_PATH | FsFlags::NO_FOLLOW_LAST,
        ..Default::default()
    }];
    syscall_path_handler(request, "lchown", argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.

        let req = request.scmpreq;
        let owner = libc::uid_t::try_from(req.data.args[1])
            .map(Uid::from_raw)
            .ok();
        let group = libc::gid_t::try_from(req.data.args[2])
            .map(Gid::from_raw)
            .ok();
        syscall_chown_handler(request, path_args, owner, group)
    })
}

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

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

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

    let empty = flags.contains(AtFlags::AT_EMPTY_PATH);
    let mut flags = SysFlags::empty();
    if empty {
        flags |= SysFlags::EMPTY_PATH;
    }

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

    syscall_path_handler(request, "fchownat", argv, |path_args, request, sandbox| {
        drop(sandbox); // release the read-lock.

        let owner = libc::uid_t::try_from(req.data.args[2])
            .map(Uid::from_raw)
            .ok();
        let group = libc::gid_t::try_from(req.data.args[3])
            .map(Gid::from_raw)
            .ok();
        syscall_chown_handler(request, path_args, owner, group)
    })
}

/// A helper function to handle chown, lchown, and fchownat syscalls.
fn syscall_chown_handler(
    request: &UNotifyEventRequest,
    args: PathArgs,
    owner: Option<Uid>,
    group: Option<Gid>,
) -> Result<ScmpNotifResp, Errno> {
    if owner.is_none() && group.is_none() {
        // Nothing to change.
        return Ok(request.return_syscall(0));
    }

    // 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 chown, report a bug!"
    );
    let fd = path.dir.as_ref().map(|fd| fd.as_fd()).ok_or(Errno::EBADF)?;

    safe_fchown(fd, owner, group).map(|_| request.return_syscall(0))
}
