//
// 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

// SAFETY: This module has been liberated from unsafe code!
#![forbid(unsafe_code)]

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

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

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

        // SysArg has one element.
        // We use MUST_PATH, dir refers to the file.
        #[expect(clippy::disallowed_methods)]
        let path = path_args.0.as_ref().unwrap();
        assert!(path.base().is_empty()); // MUST_PATH!

        let req = request.scmpreq;

        // Linux kernel truncates upper bits.
        #[expect(clippy::cast_possible_truncation)]
        let owner = match req.data.args[1] as u32 {
            u32::MAX => None,
            n => Some(Uid::from_raw(n)),
        };
        #[expect(clippy::cast_possible_truncation)]
        let group = match req.data.args[2] as u32 {
            u32::MAX => None,
            n => Some(Gid::from_raw(n)),
        };

        if owner.is_none() && group.is_none() {
            // Nothing to change.
            return Ok(request.return_syscall(0));
        }

        safe_fchown(path.dir(), 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;

        // Linux kernel truncates upper bits.
        #[expect(clippy::cast_possible_truncation)]
        let owner = match req.data.args[1] as u32 {
            u32::MAX => None,
            n => Some(Uid::from_raw(n)),
        };
        #[expect(clippy::cast_possible_truncation)]
        let group = match req.data.args[2] as u32 {
            u32::MAX => None,
            n => Some(Gid::from_raw(n)),
        };

        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;

        // Linux kernel truncates upper bits.
        #[expect(clippy::cast_possible_truncation)]
        let owner = match req.data.args[1] as u32 {
            u32::MAX => None,
            n => Some(Uid::from_raw(n)),
        };
        #[expect(clippy::cast_possible_truncation)]
        let group = match req.data.args[2] as u32 {
            u32::MAX => None,
            n => Some(Gid::from_raw(n)),
        };

        syscall_chown_handler(request, path_args, owner, group)
    })
}

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

    // 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.

        // Linux kernel truncates upper bits.
        #[expect(clippy::cast_possible_truncation)]
        let owner = match req.data.args[2] as u32 {
            u32::MAX => None,
            n => Some(Uid::from_raw(n)),
        };
        #[expect(clippy::cast_possible_truncation)]
        let group = match req.data.args[3] as u32 {
            u32::MAX => None,
            n => Some(Gid::from_raw(n)),
        };

        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));
    }

    // SysArg has one element.
    // We use MUST_PATH, dir refers to the file.
    #[expect(clippy::disallowed_methods)]
    let path = args.0.as_ref().unwrap();
    assert!(path.base().is_empty()); // MUST_PATH!

    safe_fchownat(path.dir(), owner, group).map(|_| request.return_syscall(0))
}
